diff --git a/.bazelrc b/.bazelrc index 1eb227359..9d95ea4da 100644 --- a/.bazelrc +++ b/.bazelrc @@ -56,16 +56,33 @@ common:x86_64-qnx --config=_toolchain_common common:x86_64-qnx --platforms=@score_bazel_platforms//:x86_64-qnx-sdp_8.0.0-posix common:x86_64-qnx --extra_toolchains=@score_qcc_x86_64_toolchain//:all common:x86_64-qnx --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_x86_64_pc_nto_qnx800 +common:x86_64-qnx --extra_toolchains=@score_qnx_x86_64_ifs_toolchain//:all common:x86_64-qnx --repo_env=TARGET_ARCH=x86_64 common:aarch64-qnx --config=_toolchain_common common:aarch64-qnx --platforms=@score_bazel_platforms//:aarch64-qnx-sdp_8.0.0-posix common:aarch64-qnx --extra_toolchains=@score_qcc_aarch64_toolchain//:all common:aarch64-qnx --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_aarch64_unknown_nto_qnx800 +common:aarch64-qnx --extra_toolchains=@score_qnx_aarch64_ifs_toolchain//:all common:aarch64-qnx --repo_env=TARGET_ARCH=aarch64 common --credential_helper=*.qnx.com=%workspace%/.github/tools/qnx_credential_helper.py +# Force linux-sandbox to integration test execution only. +# All integration tests have to have "integration_test" in their rule name, +# otherwise they will be executed without sandboxing, which is not desired. +# QEMU-based integration tests must run in linux-sandbox because they modify +# network interfaces and IP configuration. Use this config for those runs: +# bazel test --config=qemu-integration //... +test:qemu-integration --strategy_regexp='^Testing .*integration_test.*=linux-sandbox' +test:qemu-integration --//quality/integration_testing/flags:linux_backend=qemu +test:qemu-integration --run_under=@score_itf//scripts:run_under_qemu +test:x86_64-qnx --strategy_regexp='^Testing .*integration_test.*=linux-sandbox' +test:x86_64-qnx --run_under=@score_itf//scripts:run_under_qemu +test:aarch64-qnx --strategy_regexp='^Testing .*integration_test.*=linux-sandbox' +test:aarch64-qnx --run_under=@score_itf//scripts:run_under_qemu + + test --test_output=errors build --incompatible_strict_action_env diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b400e248d..4bd5ce896 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -28,5 +28,7 @@ RUN if [ "$USERNAME" != "vscode" ]; then \ && chmod 0440 /etc/sudoers.d/${USERNAME}; \ fi +RUN apt-get update && apt-get install -y qemu-system-x86 iputils-ping tcpdump cloud-image-utils + # Set the default user for the container USER ${USERNAME} diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 000000000..fbc79c198 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,9 @@ +{ + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:3": { + "version": "3.0.1", + "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:ca2508495b01ba29eba93e8153772a2daa65eaa86471cc6863fe2a3d21933df9", + "integrity": "sha256:ca2508495b01ba29eba93e8153772a2daa65eaa86471cc6863fe2a3d21933df9" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 98f28b0d8..b5729cd15 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,8 @@ }, "remoteUser": "${localEnv:USER}", "runArgs": [ - "--privileged" + "--privileged", + "--device=/dev/kvm" ], "mounts": [ { @@ -22,6 +23,9 @@ "type": "bind" } ], + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:3": {} + }, "initializeCommand": { "Make sure QNX licenses exists": "mkdir -p ~/.qnx/license && touch -a ~/.qnx/license/licenses", "Make sure .netrc exists": "touch -a ~/.netrc" @@ -29,6 +33,7 @@ "onCreateCommand": { "Setup external symlink": "[ -L external ] || ln -s \"bazel-$(basename $PWD)/external\" external" }, + "postStartCommand": "sudo chmod 666 /dev/kvm", "customizations": { "vscode": { "extensions": [ diff --git a/.github/workflows/build_and_test_host.yml b/.github/workflows/build_and_test_host.yml index 5317842b7..1dab8943e 100644 --- a/.github/workflows/build_and_test_host.yml +++ b/.github/workflows/build_and_test_host.yml @@ -47,3 +47,37 @@ jobs: bazel test //... --build_tests_only - run: df -h if: always() + + build_and_int_qemu_test_host: + runs-on: ubuntu-24.04 + steps: + - uses: eclipse-score/more-disk-space@v1 + - name: Checkout repository + uses: actions/checkout@v6 + - name: Create Bazel output base directory + run: | + sudo mkdir -p /mnt/.bazel + sudo chown -R $USER:$USER /mnt/.bazel + - name: Create config indicator file + run: echo "host" > .bazel_config + - name: Setup Bazel + uses: eclipse-score/cicd-actions/setup-bazel-cache@659dcbed63f6b7fbde88c7850125ea0a0a92f939 + with: + unique-cache-name: ${{ github.workflow }}-${{ github.job }} + - name: Install tools needed for ITF tests + run: | + sudo apt-get update + sudo apt-get install -y qemu-system-x86 iputils-ping tcpdump cloud-image-utils + - name: Allow linux-sandbox + uses: eclipse-score/cicd-actions/unblock-user-namespace-for-linux-sandbox@659dcbed63f6b7fbde88c7850125ea0a0a92f939 + - run: df -h + - name: Enable KVM group permissons + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Run Linux QEMU integration tests + run: | + bazel test --config=qemu-integration //quality/... //tests/integration_test/... + - run: df -h + if: always() diff --git a/.github/workflows/build_and_test_qnx.yml b/.github/workflows/build_and_test_qnx.yml index 4c54937d1..0a60f8690 100644 --- a/.github/workflows/build_and_test_qnx.yml +++ b/.github/workflows/build_and_test_qnx.yml @@ -30,6 +30,7 @@ jobs: with: # TODO: build all targets ( //... ) once python toolchain issue is resolved. bazel-target: "//src/... //tests/... //examples/..." + bazel-test-target: "//quality/... //tests/integration_test/..." bazel-config: ${{ matrix.bazel-config }} credential-helper: ".github/tools/qnx_credential_helper.py" bazel-disk-cache: ${{ github.workflow }}-${{ matrix.bazel-config }} diff --git a/MODULE.bazel b/MODULE.bazel index 22a0c2a8e..b9950d840 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -120,7 +120,9 @@ use_repo( "score_gcc_aarch64_toolchain", "score_gcc_x86_64_toolchain", "score_qcc_aarch64_toolchain", + "score_qcc_aarch64_toolchain_pkg", "score_qcc_x86_64_toolchain", + "score_qcc_x86_64_toolchain_pkg", ) # ============================================================================ @@ -311,3 +313,36 @@ http_archive( "https://github.com/COVESA/vsomeip/archive/refs/tags/3.6.1.tar.gz", ], ) + +# ============================================================================ +# Integration Testing +# ============================================================================ +bazel_dep(name = "score_itf", version = "0.4.0", dev_dependency = True) +bazel_dep(name = "score_rules_imagefs", version = "0.0.3", dev_dependency = True) +bazel_dep(name = "rules_oci", version = "2.2.7", dev_dependency = True) + +http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") + +http_file( + name = "ubuntu_24_04_cloudimg", + downloaded_file_path = "noble-server-cloudimg-amd64.img", + integrity = "sha256-X6WwXl7COYWMRTFIXWAjsIlkSMLffGOzT42ubqYFGkQ=", + url = "https://cloud-images.ubuntu.com/releases/noble/release-20260615/ubuntu-24.04-server-cloudimg-amd64.img", +) + +imagefs = use_extension("@score_rules_imagefs//extensions:imagefs.bzl", "imagefs", dev_dependency = True) +imagefs.toolchain( + name = "score_qnx_x86_64_ifs_toolchain", + sdp_to_import = "@score_qcc_x86_64_toolchain_pkg", + target_cpu = "x86_64", + target_os = "qnx", + type = "ifs", +) +imagefs.toolchain( + name = "score_qnx_aarch64_ifs_toolchain", + sdp_to_import = "@score_qcc_aarch64_toolchain_pkg", + target_cpu = "aarch64", + target_os = "qnx", + type = "ifs", +) +use_repo(imagefs, "score_qnx_aarch64_ifs_toolchain", "score_qnx_x86_64_ifs_toolchain") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index ae7d35a9a..a8b82581f 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -38,6 +38,7 @@ "https://bcr.bazel.build/modules/aspect_bazel_lib/2.20.0/MODULE.bazel": "c5565bac49e1973227225b441fad1c938d498d83df62dc5da95b2fab0f0626a2", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.0/MODULE.bazel": "7fe0191f047d4fe4a4a46c1107e2350cbb58a8fc2e10913aa4322d3190dec0bf", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.0/source.json": "369df5b7f2eae82f200fff95cf1425f90dee90a0d0948122060b48150ff0e224", + "https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.2/MODULE.bazel": "780d1a6522b28f5edb7ea09630748720721dfe27690d65a2d33aa7509de77e07", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.9.3/MODULE.bazel": "66baf724dbae7aff4787bf2245cc188d50cb08e07789769730151c0943587c14", @@ -472,6 +473,8 @@ "https://bcr.bazel.build/modules/rules_nodejs/6.5.2/MODULE.bazel": "7f9ea68a0ce6d82905ce9f74e76ab8a8b4531ed4c747018c9d76424ad0b3370d", "https://bcr.bazel.build/modules/rules_nodejs/6.7.3/MODULE.bazel": "c22a48b2a0dbf05a9dc5f83837bbc24c226c1f6e618de3c3a610044c9f336056", "https://bcr.bazel.build/modules/rules_nodejs/6.7.3/source.json": "a3f966f4415a8a6545e560ee5449eac95cc633f96429d08e87c87775c72f5e09", + "https://bcr.bazel.build/modules/rules_oci/2.2.7/MODULE.bazel": "f6150e4b224d459f7f6523ef65967464ca4efdd266c7fbf2f5a2a51011957e0c", + "https://bcr.bazel.build/modules/rules_oci/2.2.7/source.json": "b099f02af330f47f19dc67fc9300ef6e1937a8c86882690db0e7a2fcea8c7f6b", "https://bcr.bazel.build/modules/rules_perl/1.1.0/MODULE.bazel": "22138e75bb8f1ee6c21f609b90d2c24b0c9b796ccf55cc04c1c9190b699f7e9d", "https://bcr.bazel.build/modules/rules_perl/1.1.0/source.json": "896fe7707a38c5b229c6f5fa77134209874c4d57fecda5f756c1f23e4d25aae2", "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", @@ -602,6 +605,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.20.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.22.0/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.7.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/aspect_bazel_lib/2.9.3/MODULE.bazel": "not found", @@ -928,6 +932,7 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_nodejs/6.3.3/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_nodejs/6.5.2/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_nodejs/6.7.3/MODULE.bazel": "not found", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_oci/2.2.7/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_perl/1.1.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_pkg/0.7.0/MODULE.bazel": "not found", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/rules_pkg/1.0.1/MODULE.bazel": "not found", @@ -1000,6 +1005,8 @@ "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_devcontainer/1.7.0/source.json": "a3f55522fd9f63fae7a92f3cb5f91c25ae7474a39e9f9c633f0cf797fc0ca8e5", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.5.0/MODULE.bazel": "4cfe52fe8b8dbeaf7e87500036391da278f72f1c2b41b689ffdd4337196dd8fe", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_docs_as_code/4.5.0/source.json": "e01b29a3e9640a0d41d880d7da525e451115649d90f097ed66d09808c9135486", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.4.0/MODULE.bazel": "dd3e225e6fea37ab0c950888f4785faa4485215c15dfc5082ea8c577709db4e9", + "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_itf/0.4.0/source.json": "95fc7a5446339690dd40700ae3cab0492257cfd0269d402f42531af1e3278734", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.6.0/MODULE.bazel": "2496bc24311f69f49449ee85d8bb38e3b970cbfcf10d0a7f19b2d5262ce80e8d", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_process/1.6.0/source.json": "093424aa8bfed8705a3d142b21fe1d053258f2dd5eb1944941f6439b2c7157e9", "https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/modules/score_qnx_unit_tests/0.1.0/MODULE.bazel": "3d06447a9b343838883dc64d0d8c21dc9bfac6d4a1f1885a5b3ed3dcfc04ded5", @@ -2375,6 +2382,163 @@ "recordedRepoMappingEntries": [] } }, + "@@rules_oci+//oci:extensions.bzl%oci": { + "general": { + "bzlTransitiveDigest": "p8j9p1TY6GfGMUXIvm4XUoNA74auxUauN/inMq6ykQs=", + "usagesDigest": "mk7b64EVZSSr2mm3Csmke8hl4/SRvAf9rQ34V0wBYus=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "oci_crane_darwin_amd64": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%crane_repositories", + "attributes": { + "platform": "darwin_amd64", + "crane_version": "v0.18.0" + } + }, + "oci_crane_darwin_arm64": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%crane_repositories", + "attributes": { + "platform": "darwin_arm64", + "crane_version": "v0.18.0" + } + }, + "oci_crane_linux_arm64": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%crane_repositories", + "attributes": { + "platform": "linux_arm64", + "crane_version": "v0.18.0" + } + }, + "oci_crane_linux_armv6": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%crane_repositories", + "attributes": { + "platform": "linux_armv6", + "crane_version": "v0.18.0" + } + }, + "oci_crane_linux_i386": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%crane_repositories", + "attributes": { + "platform": "linux_i386", + "crane_version": "v0.18.0" + } + }, + "oci_crane_linux_s390x": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%crane_repositories", + "attributes": { + "platform": "linux_s390x", + "crane_version": "v0.18.0" + } + }, + "oci_crane_linux_amd64": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%crane_repositories", + "attributes": { + "platform": "linux_amd64", + "crane_version": "v0.18.0" + } + }, + "oci_crane_windows_armv6": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%crane_repositories", + "attributes": { + "platform": "windows_armv6", + "crane_version": "v0.18.0" + } + }, + "oci_crane_windows_amd64": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%crane_repositories", + "attributes": { + "platform": "windows_amd64", + "crane_version": "v0.18.0" + } + }, + "oci_crane_toolchains": { + "repoRuleId": "@@rules_oci+//oci/private:toolchains_repo.bzl%toolchains_repo", + "attributes": { + "toolchain_type": "@rules_oci//oci:crane_toolchain_type", + "toolchain": "@oci_crane_{platform}//:crane_toolchain" + } + }, + "oci_regctl_darwin_amd64": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%regctl_repositories", + "attributes": { + "platform": "darwin_amd64" + } + }, + "oci_regctl_darwin_arm64": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%regctl_repositories", + "attributes": { + "platform": "darwin_arm64" + } + }, + "oci_regctl_linux_arm64": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%regctl_repositories", + "attributes": { + "platform": "linux_arm64" + } + }, + "oci_regctl_linux_s390x": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%regctl_repositories", + "attributes": { + "platform": "linux_s390x" + } + }, + "oci_regctl_linux_amd64": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%regctl_repositories", + "attributes": { + "platform": "linux_amd64" + } + }, + "oci_regctl_windows_amd64": { + "repoRuleId": "@@rules_oci+//oci:repositories.bzl%regctl_repositories", + "attributes": { + "platform": "windows_amd64" + } + }, + "oci_regctl_toolchains": { + "repoRuleId": "@@rules_oci+//oci/private:toolchains_repo.bzl%toolchains_repo", + "attributes": { + "toolchain_type": "@rules_oci//oci:regctl_toolchain_type", + "toolchain": "@oci_regctl_{platform}//:regctl_toolchain" + } + } + }, + "moduleExtensionMetadata": { + "explicitRootModuleDirectDeps": [], + "explicitRootModuleDirectDevDeps": [], + "useAllRepos": "NO", + "reproducible": false + }, + "recordedRepoMappingEntries": [ + [ + "aspect_bazel_lib+", + "bazel_tools", + "bazel_tools" + ], + [ + "bazel_features+", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_oci+", + "aspect_bazel_lib", + "aspect_bazel_lib+" + ], + [ + "rules_oci+", + "bazel_features", + "bazel_features+" + ], + [ + "rules_oci+", + "bazel_skylib", + "bazel_skylib+" + ] + ] + } + }, "@@rules_python+//python/uv:uv.bzl%uv": { "general": { "bzlTransitiveDigest": "yG9F6L2IZXKRbD/aIUa6sU7uITLUTBKOMWPbIvl1VdM=", @@ -9864,6 +10028,59 @@ ] } }, + "@@score_rules_imagefs+//extensions:imagefs.bzl%imagefs": { + "general": { + "bzlTransitiveDigest": "6ZLANe53LXwcnne1ZrZm9GkJ9H5VdsCLSgSNHyUrqA4=", + "usagesDigest": "0aAEO7jwq/7LkEoK63ZByhgZ5oL8fbHIl69hKAf9nb4=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "score_qnx_x86_64_ifs_toolchain": { + "repoRuleId": "@@score_rules_imagefs+//rules/qnx:imagefs_toolchain.bzl%imagefs_toolchain", + "attributes": { + "tc_cpu": "x86_64", + "tc_os": "qnx", + "tc_pkg_repo": "'@@score_bazel_cpp_toolchains++gcc+score_qcc_x86_64_toolchain_pkg'", + "sdp_version": "8.0.0", + "tc_type": "ifs" + } + }, + "score_qnx_aarch64_ifs_toolchain": { + "repoRuleId": "@@score_rules_imagefs+//rules/qnx:imagefs_toolchain.bzl%imagefs_toolchain", + "attributes": { + "tc_cpu": "aarch64", + "tc_os": "qnx", + "tc_pkg_repo": "'@@score_bazel_cpp_toolchains++gcc+score_qcc_aarch64_toolchain_pkg'", + "sdp_version": "8.0.0", + "tc_type": "ifs" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "", + "score_qcc_aarch64_toolchain_pkg", + "score_bazel_cpp_toolchains++gcc+score_qcc_aarch64_toolchain_pkg" + ], + [ + "", + "score_qcc_x86_64_toolchain_pkg", + "score_bazel_cpp_toolchains++gcc+score_qcc_x86_64_toolchain_pkg" + ], + [ + "score_rules_imagefs+", + "bazel_tools", + "bazel_tools" + ], + [ + "score_rules_imagefs+", + "score_rules_imagefs", + "score_rules_imagefs+" + ] + ] + } + }, "@@score_toolchains_rust+//extensions:ferrocene_toolchain_ext.bzl%ferrocene_rules_rust_miri_toolchain_ext": { "general": { "bzlTransitiveDigest": "JTR6scmBZuukTYspnbvaNFU27CfBHiQyu6OywcI1QPE=", diff --git a/README.md b/README.md index f29c5796d..57446bb4f 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ Finally start the benchmark on the someipd-1 container in a third shell: docker exec -it docker_setup-someipd-1 /home/source/bazel-bin/tests/benchmarks/ipc_benchmarks ``` +For current Bazel-based integration testing backends (Docker, Linux QEMU, and QNX QEMU) and defaults, see [quality/README.md](quality/README.md). + ## 📝 Configuration diff --git a/REUSE.toml b/REUSE.toml index 8c2dbb00b..975a375b9 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -48,7 +48,12 @@ path = [ "tests/integration/sample_client/vsomeip.json", "tests/integration/vsomeip-local.json", "tests/integration/vsomeip.json", - "tests/benchmarks/config/benchmark_mw_com_config.json" + "tests/integration_test/sample_client/vsomeip.json", + "tests/integration_test/vsomeip-local.json", + "tests/integration_test/vsomeip.json", + "tests/benchmarks/config/benchmark_mw_com_config.json", + "quality/integration_testing/environments/qnx8_qemu/qemu_config.json", + "quality/integration_testing/environments/ubuntu24_04_qemu/*" ] SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" SPDX-License-Identifier = "Apache-2.0" diff --git a/quality/README.md b/quality/README.md new file mode 100644 index 000000000..ce44b5090 --- /dev/null +++ b/quality/README.md @@ -0,0 +1,74 @@ + + +# Quality Infrastructure + +This directory contains quality-related Bazel configuration used by tests in this repository. + +## Contents + +- `integration_testing/`: shared macro and environment/plugin artifacts for integration tests. + +## Integration Testing + +The integration test entry point is the `integration_test(...)` macro in `integration_testing/integration_testing.bzl`. + +See [integration_testing/test](integration_testing/test/BUILD) for an example usage. + +## Typical Usage + +Run integration tests with defaults (Linux Docker backend): + +```bash +bazel test //tests/integration:integration +``` + +Run integration tests on Linux QEMU backend: + +```bash +bazel test //tests/integration:integration \ + --//quality/integration_testing/flags:linux_backend=qemu +``` + +### Execution Backend Selection + +Integration tests are enabled only for QEMU-backed runs: + +- Linux QEMU backend: selected when `linux_backend = "qemu"` on Linux. +- QNX backend: selected by target platform (`@platforms//os:qnx`) and uses QNX QEMU artifacts. +- Any other Linux backend value keeps integration tests incompatible, so they are skipped. + +### filesystem tar + +The tests and their data are packaged into a filesystem tar and either build into (Docker) the image or uploaded after image startup. + +### What Runs in QEMU + +#### Linux QEMU + +If QEMU is selected, the custom plugin [`linux_qemu`](integration_testing/plugins/linux_qemu/README.md) is used. + +#### QNX QEMU + +For QNX targets, the macro builds a QNX IFS image and passes it to the upstream QEMU ITF plugin with the QNX QEMU config. + +### Default Test Parameters Applied by the Macro + +Unless explicitly overridden by the test target: + +- `size = "enormous"` +- `timeout = "short"` + +The macro also always injects `--log-cli-level=DEBUG` for all backends. diff --git a/quality/integration_testing/BUILD b/quality/integration_testing/BUILD new file mode 100644 index 000000000..14ee82851 --- /dev/null +++ b/quality/integration_testing/BUILD @@ -0,0 +1,18 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +filegroup( + name = "pip_requirements", + srcs = ["requirements.txt"], + visibility = ["//:__pkg__"], +) diff --git a/quality/integration_testing/environments/BUILD b/quality/integration_testing/environments/BUILD new file mode 100644 index 000000000..6bdeed2c5 --- /dev/null +++ b/quality/integration_testing/environments/BUILD @@ -0,0 +1,12 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* diff --git a/quality/integration_testing/environments/qnx8_qemu/BUILD b/quality/integration_testing/environments/qnx8_qemu/BUILD new file mode 100644 index 000000000..5340986db --- /dev/null +++ b/quality/integration_testing/environments/qnx8_qemu/BUILD @@ -0,0 +1,38 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_pkg//pkg:mappings.bzl", "pkg_files") + +filegroup( + name = "init_build", + srcs = ["init_x86_64.build"], + visibility = ["//:__subpackages__"], +) + +exports_files( + ["qemu_config.json"], + visibility = ["//:__subpackages__"], +) + +filegroup( + name = "qemu_config", + srcs = ["qemu_config.json"], + visibility = ["//:__subpackages__"], +) + +pkg_files( + name = "qnx_config", + srcs = ["qcrypto.conf"], + prefix = "/etc", + visibility = ["//:__subpackages__"], +) diff --git a/quality/integration_testing/environments/qnx8_qemu/init_x86_64.build b/quality/integration_testing/environments/qnx8_qemu/init_x86_64.build new file mode 100644 index 000000000..99ff35ad5 --- /dev/null +++ b/quality/integration_testing/environments/qnx8_qemu/init_x86_64.build @@ -0,0 +1,510 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +############################################################################### +# +# Example image built based on minimal configuration from QNX +# +############################################################################### + +[-optional] + +[image=0x3600000] +[virtual=x86_64,multiboot] boot = { + startup-x86 -v -D8250..115200 -zz + PATH=/proc/boot + LD_LIBRARY_PATH=/proc/boot + [+keeplinked] procnto-smp-instr +} + +[+script] startup-script = { + procmgr_symlink /dev/shmem /tmp + display_msg Welcome to QNX OS 8.0 on x86_64 tweaked for S-CORE! + # These env variables get inherited by all programs which follow + SYSNAME=nto + TERM=qansi + + devc-ser8250 & + waitfor /dev/ser1 + reopen /dev/ser1 + + /proc/boot/startup.sh + + [+session] /bin/sh & +} + +[uid=0 gid=0 perms=0700] startup.sh = { + # Enable logging via slogger2 + echo "---> Starting slogger2" + slogger2 -s 4096 # Start system logger with 4KB buffer size for log messages + waitfor /dev/slog # Wait for system log device to become available + + # PCI server required for virtio-net (QEMU networking) + echo "---> Starting PCI Services" + pci-server --config=/proc/boot/pci_server.cfg + waitfor /dev/pci + + # Required to support POSIX pipes (used e.g. by service discovery) + echo "---> Starting Pipe" + pipe + waitfor /dev/pipe + + # Start random number generator service (provides /dev/random + # needed by C++ std::random_device and other consumers). + echo "---> Starting Random" + random + waitfor /dev/random + + # Service discovery depends on inotify, which requires fsevmgr + echo "---> Starting fsevmgr" + fsevmgr + waitfor /dev/fsnotify + + # Create a RAM disk and mount it at /tmp_discovery + # Block size reduced to the minimum, since we only put flag files without content. + # 4096 inodes for approx. 1360 service offers (~2-3 files per service offer) + echo "---> Starting devb-ram" + devb-ram ram capacity=1 blk ramdisk=10m,cache=512k,vnode=256 2>/dev/null + waitfor /dev/ram0 + + echo "---> Mounting RAM disk" + mkqnx6fs -q -b 512 -i 4096 /dev/ram0 + mount -t qnx6 /dev/ram0 /tmp_discovery + chmod 777 /tmp_discovery + + echo "---> Starting mqueue" + mqueue # Start POSIX message queue resource manager + waitfor /dev/mqueue # Wait for message queue device to be available + + echo "---> Starting devc-pty" + devc-pty -n 32 + + echo "---> Starting devb-eide" + devb-eide cam user=20:20 blk cache=64M,auto=partition,vnode=2000,ncache=2000,commit=low # Start IDE/SATA block driver + waitfor /dev/hd0 # Wait for first hard disk to be detected + + # Networking stack for SSH-based ITF test harness communication + echo "---> Starting Networking" + io-sock -m phy -m pci -d vtnet_pci + waitfor /dev/socket + + echo "---> Configuring network interface" + # Bring up the interface and configure with bridge-accessible IP for direct ping + if_up -p vtnet0 + # Bring up the vtnet0 network interface in promiscuous mode + ifconfig vtnet0 169.254.158.190 netmask 255.255.0.0 # Configure IP address and subnet mask for vtnet0 + ifconfig vtnet0 alias 10.0.2.15 netmask 255.255.255.0 # Configure additional IP address and subnet mask for vtnet0 + # for usage with QEMU port forwarding + + # add ip multicast route for service discovery to work (multicast address range: 224.0.0.0/4) + # route add -net 224.0.0.0/4 169.254.158.190 + route add -net 224.0.0.0/4 169.254.21.88 + + # Configure system network settings + sysctl -w net.inet.icmp.bmcastecho=1 # Enable ICMP broadcast echo (responds to broadcast pings) + sysctl -w qnx.sec.droproot=33:33 # Set user/group ID (33:33) for dropping root privileges + + echo "---> Setting hostname" + hostname qnx-score # Set default hostname if no file exists + echo "Qnx_S-core" > /tmp/hostname # Create temporary hostname file + echo "Default hostname set to: Qnx_S-core" + + /proc/boot/sshd -f /var/ssh/sshd_config # Start SSH daemon with specified configuration file +} + + + + +############################################# +### SHARED LIBRARIES ### +############################################# +/usr/lib/ldqnx-64.so.2=ldqnx-64.so.2 # 64-bit QNX dynamic linker (loads shared libraries) + +cam-cdrom.so # CAM (Common Access Method) CD-ROM driver +cam-disk.so # CAM disk driver for storage devices +fs-dos.so # DOS file system support +fs-qnx6.so # QNX6 file system support +io-blk.so # Block I/O manager +libbz2.so.1 # BZip2 compression library +libc++.so.2 # C++ standard library +libc.so # C standard library (essential) +libc.so.6 # Main C library (malloc, printf, file I/O, etc.) +libcam.so.2 # CAM library for device access +libcatalog.so.1 # Message catalog support +libcrypto.so.3 # OpenSSL crypto library (disabled) +libexpat.so.2 # Expat XML parser library +libfscrypto.so.1 # File system encryption support +libfsnotify.so.1 # File system notification library +libgcc_s.so.1 # GCC runtime support library +libiconv.so.1 # Character encoding conversion +libjail.so.1 # QNX jail library for process containment +liblzma.so.5 # LZMA compression library +libm.so # Math library +libpam.so.2 # Pluggable Authentication Modules library +libpci.so.3.0 # PCI library v3.0 for hardware access +libqcrypto.so.1.0 # QNX cryptographic library +libqh.so # QNX helper library +libregex.so.1 # Regular expression library +libsecpol.so.1 # Security policy library +libslog2.so.1 # System logging library (slog2_* functions) +libslog2parse.so.1 # System log parsing library +libsocket.so.4 # Socket library (TCP/IP, UDP, network I/O functions) +libssl.so.3 # OpenSSL SSL/TLS library (disabled) +libtracelog.so.1 # Trace logging library +libxml2.so.2 # XML parsing library +libz.so # Zlib compression library +qcrypto-openssl-3.so # QNX cryptographic library with OpenSSL 3 support + +[type=link] libslog2.so=libslog2.so.1 # Create version-neutral symbolic link +[type=link] libsocket.so=libsocket.so.4 # Create version-neutral symbolic link +[type=link] qcrypto-openssl.so=qcrypto-openssl-3.so + + +############################################# +### Tools ### +############################################# +[type=link] /bin/ls=/proc/boot/ls # Link ls to IFS version for compatibility +toybox # Minimal Unix utilities collection (replaces many GNU tools) +[type=link] cp=toybox # Copy files and directories +[type=link] ls=toybox # List directory contents +[type=link] cat=toybox # Display file contents +[type=link] chmod=toybox # Change file permissions +[type=link] rm=toybox # Remove files and directories +[type=link] dd=toybox # Convert and copy files with specified I/O block size +[type=link] echo=toybox # Display text +[type=link] grep=toybox # Search text patterns in files +[type=link] mkdir=toybox # Create directories +[type=link] ascii=toybox # Display ASCII character set +[type=link] base64=toybox # Base64 encoding/decoding utility +[type=link] basename=toybox # Extract filename from path +[type=link] bc=toybox # Basic calculator +[type=link] bunzip2=toybox # Decompress bzip2 files +[type=link] bzcat=toybox # Display contents of bzip2 files +[type=link] cal=toybox # Display calendar +[type=link] chgrp=toybox # Change group ownership +[type=link] chown=toybox # Change file ownership +[type=link] cksum=toybox # Calculate checksums +[type=link] clear=toybox # Clear terminal screen +[type=link] cmp=toybox # Compare files byte by byte +[type=link] comm=toybox # Compare sorted files line by line +[type=link] cpio=toybox # Copy files to/from archives +[type=link] crc32=toybox # Calculate CRC32 checksums +[type=link] cut=toybox # Extract columns from files +[type=link] date=toybox # Display or set system date +[type=link] diff=toybox # Compare files line by line +[type=link] dirname=toybox # Extract directory from path +[type=link] dos2unix=toybox # Convert DOS line endings to Unix +[type=link] du=toybox # Display disk usage +[type=link] egrep=toybox # Extended grep with regular expressions +[type=link] env=toybox # Display or set environment variables +[type=link] expand=toybox # Convert tabs to spaces +[type=link] expr=toybox # Evaluate expressions +[type=link] false=toybox # Return false status +[type=link] fgrep=toybox # Fast grep for fixed strings +[type=link] file=toybox # Determine file type +[type=link] find=toybox # Search for files and directories +[type=link] fmt=toybox # Format text paragraphs +[type=link] groups=toybox # Display user group membership +[type=link] gunzip=toybox # Decompress gzip files +[type=link] gzip=toybox # Compress files with gzip +[type=link] hd=toybox # Hexadecimal dump (alias for hexdump) +[type=link] head=toybox # Display first lines of files +[type=link] hexdump=toybox # Display files in hexadecimal format +[type=link] id=toybox # Display user and group IDs +[type=link] install=toybox # Copy files and set attributes +[type=link] link=toybox # Create hard links +[type=link] logname=toybox # Display login name +[type=link] md5sum=toybox # Calculate MD5 checksums +[type=link] mkfifo=toybox # Create named pipes (FIFOs) +[type=link] mktemp=toybox # Create temporary files/directories +[type=link] more=toybox # Display files page by page +[type=link] mv=toybox # Move/rename files and directories +[type=link] nl=toybox # Number lines in files +[type=link] nohup=toybox # Run commands immune to hangups +[type=link] od=toybox # Dump files in octal format +[type=link] paste=toybox # Merge lines from files +[type=link] patch=toybox # Apply patches to files +[type=link] printenv=toybox # Print environment variables +[type=link] printf=toybox # Format and print data +[type=link] pwd=toybox # Print working directory +[type=link] readlink=toybox # Display target of symbolic links +[type=link] realpath=toybox # Display absolute path +[type=link] rmdir=toybox # Remove empty directories +[type=link] sed=toybox # Stream editor for filtering/transforming text +[type=link] seq=toybox # Generate sequences of numbers +[type=link] sha1sum=toybox # Calculate SHA1 checksums +[type=link] sleep=toybox # Suspend execution for specified time +[type=link] sort=toybox # Sort lines in text files +[type=link] split=toybox # Split files into pieces +[type=link] stat=toybox # Display file/filesystem status +[type=link] strings=toybox # Extract printable strings from files +[type=link] tail=toybox # Display last lines of files +[type=link] tee=toybox # Copy input to files and stdout +[type=link] test=toybox # Evaluate conditional expressions +[type=link] time=toybox # Time command execution +[type=link] timeout=toybox # Run command with time limit +[type=link] touch=toybox # Update file timestamps +[type=link] tr=toybox # Translate or delete characters +[type=link] true=toybox # Return true status +[type=link] truncate=toybox # Truncate files to specified size +[type=link] tty=toybox # Display terminal name +[type=link] uname=toybox # Display system information +[type=link] uniq=toybox # Remove duplicate lines +[type=link] unix2dos=toybox # Convert Unix line endings to DOS +[type=link] unlink=toybox # Remove files (system call interface) +[type=link] uudecode=toybox # Decode uuencoded files +[type=link] uuencode=toybox # Encode files using uuencoding +[type=link] uuidgen=toybox # Generate UUIDs +[type=link] wc=toybox # Count lines, words, and characters +[type=link] which=toybox # Locate commands in PATH +[type=link] whoami=toybox # Display current username +[type=link] xargs=toybox # Execute commands from standard input +[type=link] xxd=toybox # Make hexdump or reverse +[type=link] yes=toybox # Output string repeatedly +[type=link] zcat=toybox # Display contents of compressed files +[type=link] nc=toybox # Netcat for network connections and packet streaming +[type=link] netcat=toybox # Netcat alias for network connections +[type=link] pkill=slay # Process killer Linux compatibility layer + +[type=link] waitfor=on # Create symbolic link: waitfor -> on (waits for resources to become available) +[type=link] ability=on # Create symbolic link: ability -> on (manages process abilities/privileges) +[type=link] /bin/sh=/proc/boot/ksh # Create symbolic link: sh -> ksh (standard shell link) +[type=link] setconf=getconf # Create symbolic link: setconf -> getconf (sets configuration parameters) + +awk # Text processing and pattern scanning +devb-eide # Block device driver for IDE/SATA hard drives and SSDs +devb-ram # RAM disk block device manager +devc-pty # Pseudo-terminal device manager +devc-ser8250 # Serial port driver for 8250/16550 UART controllers (console access) +dhcpcd # DHCP client daemon for automatic network configuration +fsencrypt # File system encryption utility +fsevmgr # File system event manager +getconf # System configuration query utility - retrieves system parameters +hostname # Set or display system hostname +if_up # Network interface configuration +ifconfig # Network interface configuration tool +ksh # Korn shell - provides command-line interface and scripting +ln # Create file links +mkqnx6fs # Create QNX6 file systems +mount # File system mount utility - mounts/unmounts file systems +mount_ifs # Mount Image File System (IFS) +mqueue # POSIX message queue manager +on # Command execution utility - runs commands with specific options +openssl # SSL/TLS cryptographic toolkit +pci-server # PCI bus server +pdebug # Process debugger +pfctl # Packet filter control utility +pidin # Process information display - shows running processes (like 'ps' on Linux) +ping # ICMP ping utility for network diagnostics +pipe # Named pipe manager +random # Random number generator service +route # Network routing table management utility +shutdown # System shutdown utility +slay # Process termination utility +slog2info # System log viewer - displays logged messages +slogger2 # System logging daemon - collects and manages log messages +ssh # SSH client for remote connections +ssh-keygen # SSH key generation utility +sshd # SSH daemon for remote access +/usr/libexec/sshd-session=${QNX_TARGET}/${PROCESSOR}/usr/libexec/sshd-session # SSH session handler (required by sshd) +sync # Synchronize file system buffers to disk +sysctl # Configure kernel parameters at runtime +tar # Archive utility for creating/extracting tar files +tcpdump # Network packet capture tool for Wireshark analysis +umount # Unmount file systems + +/usr/lib/ssh/sftp-server=${QNX_TARGET}/${PROCESSOR}/usr/libexec/sftp-server # File transfer server to enable scp + + +############################################# +### NETWORKING COMPONENTS ### +############################################# +io-sock # Network socket manager +mods-pci.so # PCI module support for network hardware +mods-phy.so # Physical layer module for network interfaces +mods-usb.so # USB module support +libfdt.so.1 # Flattened Device Tree library +libusbdci.so.2 # USB device controller interface library +devs-vtnet_pci.so # VirtIO network device driver for QEMU/KVM +librpc.so.2 # For TCP dump + + +############################################# +### PCI COMPONENTS ### +############################################# +# PCI-related shared libraries and modules +pci/pci_hw-Intel_x86.so # Intel x86 PCI hardware support +pci/pci_slog2.so # PCI system logging support +pci/pci_cap-0x05.so # PCI capability handler for MSI +pci/pci_cap-0x10.so # PCI Express capability handler +pci/pci_cap-0x11.so # MSI-X capability handler +pci/pci_strings.so # PCI device string database +pci/pci_bkwd_compat.so # Backward compatibility support +pci/pci_debug2.so # Enhanced PCI debugging support + + +############################################# +### SYSTEM DIRECTORIES ### +############################################# +# [gid=0 uid=0 dperms=755 type=dir] /var/run # runtime data +# Create SSH and system directories with appropriate permissions +[gid=0 uid=0 dperms=755 type=dir] /var/chroot/sshd # SSH chroot directory for privilege separation +[gid=0 uid=0 dperms=700 type=dir] /var/ssh # SSH configuration and key storage directory + +[perms=0444] pci_server.cfg = { +[buscfg] +DO_BUS_CONFIG=no + +[envars] +PCI_DEBUG_MODULE=pci_debug2.so +PCI_HW_MODULE=pci_hw-Intel_x86.so +PCI_HW_CONFIG_FILE=/proc/boot/pci_hw.cfg +} + +[perms=0444] pci_hw.cfg = { +[interrupts] +B0:D17:F0 A 18 +B0:D17:F0 B 19 +B0:D17:F0 C 16 +B0:D17:F0 D 17 +B3:D0:F0 A 18 +B3:D0:F0 B 19 +B3:D0:F0 C 16 +B3:D0:F0 D 17 +} + +[uid=0 gid=0 perms=0444] qcrypto.conf = { +openssl tags=* +} + +[uid=0 gid=0 perms=0444] /etc/passwd = { +root::0:0:Superuser:/:/bin/sh +qnxuser::1000:1000:QNX User:/:/bin/sh +sshd::15:6:sshd:/:/bin/false +} + +[uid=0 gid=0 perms=0444] /etc/group = { +root::0:root +qnxuser::1000:qnxuser +sshd::6:sshd +} + +[uid=0 gid=0 perms=0444] /etc/profile = { +export TERM=qansi +export PATH=/proc/boot +export LD_LIBRARY_PATH=/proc/boot +export MAGIC=/system/etc/magic +} + +[uid=0 gid=0 perms=0400] /var/ssh/ssh_host_rsa_key = { +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAYEArR4LKpPGS2mqmnj+d4Y/0DLdYUVQ7hBaR/aYnNbZFUvO3nOFixa4 +u8pjRQbNFsERvY8o3Q1+SEg/xq+WC4vxhS9bjfMOxO29ncicvcDZR9kVOc/dNCLv6amdOy +9V/wN8rb7vy5KhPKF/uubj/HmcwRYZAYV3spR7C4OpN1tSZFBu/rcOYYYC7VxKcshEj8Yt +8QX7Bil+MIJHPLu7HTktgxDSp4Y3NUORoV9In1qM+rTJsxDHr3gfCOaN4OnQElwS7RfXx2 +BFcvA3d4JreynHocpLWUO6rrs1qjCsQZFPqQwBC7SDn8eiKy0XH+yPpioUaJ6L+YFqrk3c +KxijWKP5/aDuCQrw230mLtWatspHpNUXQSN1FQby6kqLUSV8gXX7lS91AMjdb3sqJ9ebxJ +xGARbp8ow1yRtTfqk6H04q2QgVQZ/Idso3TzQokt0K/feEdj7SwH3LsaAeMWPyo0WLUOk1 +8j46LUEayluF4UcIm8dnjoTywW9A/MS1C+2p4eTbAAAFeAU74GcFO+BnAAAAB3NzaC1yc2 +EAAAGBAK0eCyqTxktpqpp4/neGP9Ay3WFFUO4QWkf2mJzW2RVLzt5zhYsWuLvKY0UGzRbB +Eb2PKN0NfkhIP8avlguL8YUvW43zDsTtvZ3InL3A2UfZFTnP3TQi7+mpnTsvVf8DfK2+78 +uSoTyhf7rm4/x5nMEWGQGFd7KUewuDqTdbUmRQbv63DmGGAu1cSnLIRI/GLfEF+wYpfjCC +Rzy7ux05LYMQ0qeGNzVDkaFfSJ9ajPq0ybMQx694HwjmjeDp0BJcEu0X18dgRXLwN3eCa3 +spx6HKS1lDuq67NaowrEGRT6kMAQu0g5/HoistFx/sj6YqFGiei/mBaq5N3CsYo1ij+f2g +7gkK8Nt9Ji7VmrbKR6TVF0EjdRUG8upKi1ElfIF1+5UvdQDI3W97KifXm8ScRgEW6fKMNc +kbU36pOh9OKtkIFUGfyHbKN080KJLdCv33hHY+0sB9y7GgHjFj8qNFi1DpNfI+Oi1BGspb +heFHCJvHZ46E8sFvQPzEtQvtqeHk2wAAAAMBAAEAAAGAQMkziJWQ6fv7Wp/ZK0XUb8f5TU +Oxi8YW40OHzXoh93RNULaOzYSNUcnl6Jko+1D5oKUIt+Eq10Yih+qCDoQquJsGelLxvgTy +py/CaMjZB6hX5zDBKZfBjQJq0xFd73eQmz0PZHHVYWlW8c0imQOyBBiO9yDJsM0cVyzIkO +zeIqhvQWekPB74zXdybQ5BikSyQLbqQF4a2XCH1FS1K7SQMbKEAymZU5eb0nZkKS6r/87U +hOzMrgAYLS6K/hbCRXyrAlz61x2hMKTngb/ERWWilqJSGlF8Q4p2LmIxyUnll/C2cq2dvU +gMARPvZ3DL/QFl4fhLa20vTg83CQNaw6zuEAhTP8lmvDP+4DhtqUPno1T7161OpEHD/ZRb +oiwXmkwg+0yiR8a9OFiolAhwiqPhqC1W39+coEutjpbXQ1uJVoC0kWHMY9biBa7cb9Cxrq +boYLOs31sLhTWTrHgIgpsf/FSWYLF5hknZKpVhWQsfowyCtjn6uvVh+bcfUwOMCDthAAAA +wQDSHJTmW6o6uK53KE6t4c7x+G/qO2Y6FSRQuAbojL1o7gG0SYDFRR3k5fMQ/zqQ1pWrqR +5Im+InsfO5KG9489z/SCIXwPq6zev8WJjZV2P3A8qTBPzEeYmicZtJGH/sBuk8Oj/CdOKV ++GBzkqJdxruiNzLEIGS1gjserT6YJxmA5Q/j1Hiz+bSf66cHhGzmQ092tQ+gGvWPtbFn+D +YQVbp1GW8E6lbAOBew7IDcWp7LVDZuShBrDvFWtsaupZ7wYxoAAADBAOXzYeYNyrKsFCju +Qh1Bj0uVOf8lVvygGOzFA/OdqWuRr/h2aPsNYJFbdIiP61mvVd1umJ8BQDiXNlm/YpXOXu +jOm9a8BVKpMTtoohEkUbqBtv9vx+XHnhLCFYYL4GHJ9Dhj50L3djf++/HkahtZJlTkpSbt +/ukoZKI1m8MgpyC3xk2UDu1yWNizT2l1L7RnE+ZO4b5U9cONANiYBD8jcnZAvh4ld3ojo0 +iNXEVEGAKIodMUsgASxGwmoxuX3ugJIwAAAMEAwLp7eVBjg27eflRNYuW8akfDyq9zB1rp +YGo3GbPytb14WVJuyNL1tJ/B/rdFBg0IXf1FQJEL99kKJP5yDxjOx9kyXBhIWjm8arz39h +uw0wrsuRl2rrCfMABGS51GohtYmKETZ6x7/2n3l6iz/5mhRN8hW0JluJhKtj6ogO6GZERr +PTYwQs+9Q7QI1cPx7G5cEPhWhEd+OmKZuXRjhMQQ+ADj/lnUAhhGQatM9/bohJOdObWCRl +Fu+/MLbJQuA1zpAAAAAAEC +-----END OPENSSH PRIVATE KEY----- +} + +[uid=0 gid=0 perms=0444] /var/ssh/sshd_config = { +# Port number for SSH +Port 22 + +# Protocol version +Protocol 2 + +# Host key location +HostKey /var/ssh/ssh_host_rsa_key + +# Enable port forwarding +AllowTcpForwarding yes + +# Enable SCP/SFTP +Subsystem sftp /usr/lib/ssh/sftp-server + +# AUTHENTICATION SETTINGS - Simple passwordless authentication +# Enable password authentication to check for empty passwords +PasswordAuthentication yes + +# Allow empty passwords (no password required) +PermitEmptyPasswords yes + +# Disable all other authentication methods completely +PubkeyAuthentication no +ChallengeResponseAuthentication no +HostbasedAuthentication no + +# Allow root login without authentication +PermitRootLogin yes + +# Allow user environment variables +PermitUserEnvironment yes + +# Don't create PID file +PidFile none + +# Log level for debugging +LogLevel DEBUG + +# Disable strict modes for easier setup +StrictModes no + +# Disable DNS lookups for faster connection +UseDNS no + +# Maximum authentication attempts +MaxAuthTries 3 + +# Raise concurrent-connection limit so rapid SSH reconnects +# under QEMU SLiRP networking do not get dropped. +MaxStartups 100:30:200 + +# Disable per-source penalties (OpenSSH >= 9.8). All test +# connections originate from 127.0.0.1 via SLiRP port-forwarding +# and SIGKILLed async processes can trigger crash penalties that +# block all subsequent connections for 90 s. +PerSourcePenalties no + +# Disable login grace time +LoginGraceTime 30 +} diff --git a/quality/integration_testing/environments/qnx8_qemu/qcrypto.conf b/quality/integration_testing/environments/qnx8_qemu/qcrypto.conf new file mode 100644 index 000000000..67c7a46cd --- /dev/null +++ b/quality/integration_testing/environments/qnx8_qemu/qcrypto.conf @@ -0,0 +1,19 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +# Minimal qcrypto configuration for the random service. +# Uses the OpenSSL 3 plugin for cryptographic operations. +# The library auto-prepends "qcrypto-" to the plugin name, +# so "openssl-3" resolves to "qcrypto-openssl-3.so". +[random] +openssl-3 tags=random diff --git a/quality/integration_testing/environments/qnx8_qemu/qemu_config.json b/quality/integration_testing/environments/qnx8_qemu/qemu_config.json new file mode 100644 index 000000000..3bef2b9c1 --- /dev/null +++ b/quality/integration_testing/environments/qnx8_qemu/qemu_config.json @@ -0,0 +1,12 @@ +{ + "networks": [ + { + "name": "tap0", + "ip_address": "169.254.158.190", + "gateway": "169.254.21.88" + } + ], + "ssh_port": 22, + "qemu_num_cores": 2, + "qemu_ram_size": "1G" +} diff --git a/quality/integration_testing/environments/ubuntu24_04_qemu/BUILD b/quality/integration_testing/environments/ubuntu24_04_qemu/BUILD new file mode 100644 index 000000000..71e3ee604 --- /dev/null +++ b/quality/integration_testing/environments/ubuntu24_04_qemu/BUILD @@ -0,0 +1,88 @@ +# ******************************************************************************* +# 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( + [ + "qemu_config.json", + "user-data", + "meta-data", + ], + visibility = ["//:__subpackages__"], +) + +filegroup( + name = "qemu_config", + srcs = ["qemu_config.json"], + visibility = ["//:__subpackages__"], +) + +# Network config for cloud-init +# Tagged "manual" so it is only built when explicitly needed (linux_qemu tests). +genrule( + name = "network_config", + srcs = ["qemu_config.json"], + outs = ["network-config.yaml"], + cmd = """ +set -euo pipefail + +python3 - <<'PY' "$(location qemu_config.json)" "$@" +import json +import sys + +config_path, output_path = sys.argv[1:3] + +with open(config_path, "r", encoding="utf-8") as config_file: + config = json.load(config_file) + +networks = config.get("networks", []) +if not networks: + raise ValueError("qemu_config.json must define at least one network") + +network = networks[0] + +with open(output_path, "w", encoding="utf-8") as network_config: + network_config.write("version: 2\\n") + network_config.write("ethernets:\\n") + network_config.write(" testnic0:\\n") + network_config.write(" match:\\n") + network_config.write(' name: "en*"\\n') + network_config.write(" dhcp4: false\\n") + network_config.write(" dhcp6: false\\n") + network_config.write(" optional: true\\n") + network_config.write(" link-local: []\\n") + network_config.write(" addresses:\\n") + network_config.write(f" - {network['ip_address']}/16\\n") + network_config.write(" routes:\\n") + network_config.write(" - to: default\\n") + network_config.write(" on-link: true\\n") + network_config.write(f" via: {network['gateway']}\\n") +PY +""", + tags = ["manual"], + visibility = ["//:__subpackages__"], +) + +# Cloud-init seed image generation to configure the Ubuntu 24.04 QEMU image on boot. +# Tagged "manual" so it is only built when explicitly needed (linux_qemu tests). +genrule( + name = "seed_iso", + srcs = [ + ":network_config", + "user-data", + "meta-data", + ], + outs = ["seed.img"], + cmd = "cloud-localds -N $(location :network_config) $@ $(location user-data) $(location meta-data)", + tags = ["manual"], + visibility = ["//:__subpackages__"], +) diff --git a/quality/integration_testing/environments/ubuntu24_04_qemu/meta-data b/quality/integration_testing/environments/ubuntu24_04_qemu/meta-data new file mode 100644 index 000000000..15f31fe9d --- /dev/null +++ b/quality/integration_testing/environments/ubuntu24_04_qemu/meta-data @@ -0,0 +1,2 @@ +instance-id: score-integration-test +local-hostname: ubuntu-score diff --git a/quality/integration_testing/environments/ubuntu24_04_qemu/qemu_config.json b/quality/integration_testing/environments/ubuntu24_04_qemu/qemu_config.json new file mode 100644 index 000000000..b992bbb1b --- /dev/null +++ b/quality/integration_testing/environments/ubuntu24_04_qemu/qemu_config.json @@ -0,0 +1,13 @@ +{ + "networks": [ + { + "name": "tap0", + "ip_address": "169.254.158.190", + "gateway": "169.254.21.88" + } + ], + "ssh_port": 22, + "qemu_num_cores": 2, + "qemu_ram_size": "2G", + "boot_mode": "disk" +} diff --git a/quality/integration_testing/environments/ubuntu24_04_qemu/user-data b/quality/integration_testing/environments/ubuntu24_04_qemu/user-data new file mode 100644 index 000000000..877daa1b1 --- /dev/null +++ b/quality/integration_testing/environments/ubuntu24_04_qemu/user-data @@ -0,0 +1,17 @@ +#cloud-config +# Cloud-init configuration for QEMU-based integration tests. +# Enables root SSH access with empty password for the ITF test harness. +# All config is in bootcmd to ensure it runs before sshd starts. + +ssh_pwauth: true +disable_root: false + +bootcmd: + - sed -i 's/^root:[^:]*:/root::/' /etc/shadow + - printf 'PermitRootLogin yes\nPermitEmptyPasswords yes\nPasswordAuthentication yes\n' > /etc/ssh/sshd_config.d/99-integration-test.conf + - printf 'auth sufficient pam_permit.so\naccount sufficient pam_permit.so\nsession sufficient pam_permit.so\n' > /etc/pam.d/sshd + - ip route add 224.0.0.0/4 dev ens4 + +# runcmd: +# - systemctl enable ssh +# - systemctl restart ssh diff --git a/quality/integration_testing/flags/BUILD b/quality/integration_testing/flags/BUILD new file mode 100644 index 000000000..90739a120 --- /dev/null +++ b/quality/integration_testing/flags/BUILD @@ -0,0 +1,31 @@ +# ******************************************************************************* +# 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:common_settings.bzl", "string_flag") + +string_flag( + name = "linux_backend", + build_setting_default = "disabled", + values = [ + "disabled", + "qemu", + ], + visibility = ["//visibility:public"], +) + +config_setting( + name = "linux_qemu", + constraint_values = ["@platforms//os:linux"], + flag_values = {":linux_backend": "qemu"}, + visibility = ["//visibility:public"], +) diff --git a/quality/integration_testing/integration_testing.bzl b/quality/integration_testing/integration_testing.bzl new file mode 100644 index 000000000..efb6414c8 --- /dev/null +++ b/quality/integration_testing/integration_testing.bzl @@ -0,0 +1,156 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +"""Integration test macro with QEMU-only backends.""" + +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("@score_itf//:defs.bzl", "py_itf_test") +load("@score_rules_imagefs//rules/qnx:ifs.bzl", "qnx_ifs") + +def _extend_list_in_kwargs(kwargs, key, values): + kwargs[key] = kwargs.get(key, []) + values + return kwargs + +def _extend_list_in_kwargs_without_duplicates(kwargs, key, values): + kwargs_values = kwargs.get(key, []) + for value in values: + if value not in kwargs_values: + kwargs_values.append(value) + kwargs[key] = kwargs_values + return kwargs + +def integration_test(name, srcs, filesystem, **kwargs): + """Creates an integration test target with QEMU-only backends. + + On Linux, tests run only when the `linux_qemu` flag is selected. + On QNX, tests run with QEMU. + The test can be configured to use a custom QEMU image and config when running with QEMU. + + The network setup makes using linux-sandbox mandatory. + Otherwise no parallelism can be achieved. + + Args: + name: The target name. + srcs: Test source files. + filesystem: Tests and dependencies to be run. Will be added / uploaded into the OS image. The entrypoint is a py_test compatible python file. Must be created using `pkg_files()`. + **kwargs: Additional arguments passed to py_itf_test. + """ + LINUX_TARGET_COMPATIBLE_WITH = [ + "@platforms//os:linux", + ] + + # --- Linux QEMU artifacts --- + filesystem_tar = "_qemu_filesystem_{}".format(name) + pkg_tar( + name = filesystem_tar, + srcs = [filesystem], + ) + + linux_qemu_config = Label("//quality/integration_testing/environments/ubuntu24_04_qemu:qemu_config") + linux_qemu_image = Label("@ubuntu_24_04_cloudimg//file") + linux_qemu_seed_iso = Label("//quality/integration_testing/environments/ubuntu24_04_qemu:seed_iso") + + # --- QNX QEMU artifacts --- + QNX_TARGET_COMPATIBLE_WITH = [ + "@platforms//cpu:x86_64", + "@platforms//os:qnx", + ] + + qemu_image = "_init_ifs_{}".format(name) + qnx_ifs( + name = qemu_image, + out = "init_ifs_{}".format(name), + build_file = "//quality/integration_testing/environments/qnx8_qemu:init_build", + srcs = [filesystem, "//quality/integration_testing/environments/qnx8_qemu:qnx_config"], + target_compatible_with = QNX_TARGET_COMPATIBLE_WITH, + ) + + qnx_qemu_config = Label("//quality/integration_testing/environments/qnx8_qemu:qemu_config") + + # --- Wire up data deps and args based on platform + linux_backend flag --- + _extend_list_in_kwargs(kwargs, "data", select({ + "@platforms//os:qnx": [qemu_image, qnx_qemu_config], + "//quality/integration_testing/flags:linux_qemu": [ + filesystem_tar, + linux_qemu_config, + linux_qemu_image, + linux_qemu_seed_iso, + ], + "//conditions:default": [], + })) + _extend_list_in_kwargs( + kwargs, + "args", + select({ + "@platforms//os:qnx": [ + "--log-cli-level=DEBUG", + "--qemu-config=$(location {})".format(qnx_qemu_config), + "--qemu-image=$(location {})".format(qemu_image), + ], + "//quality/integration_testing/flags:linux_qemu": [ + "--log-cli-level=DEBUG", + "--qemu-config=$(location {})".format(linux_qemu_config), + "--qemu-image=$(location {})".format(linux_qemu_image), + "--qemu-seed-iso=$(location {})".format(linux_qemu_seed_iso), + "--qemu-filesystem-tar=$(location {})".format(filesystem_tar), + ], + "//conditions:default": [], + }), + ) + + # Integration tests are only executable in Linux QEMU mode or on QNX. + _extend_list_in_kwargs( + kwargs, + "target_compatible_with", + select({ + "@platforms//os:qnx": QNX_TARGET_COMPATIBLE_WITH, + "//quality/integration_testing/flags:linux_qemu": LINUX_TARGET_COMPATIBLE_WITH, + "//conditions:default": ["@platforms//:incompatible"], + }), + ) + + # Tests spin up docker or qemu which requires a significant amount of system resources. + if "size" not in kwargs: + kwargs["size"] = "enormous" + + # While we require a significant amount of system resources, the tests are still short. + if "timeout" not in kwargs: + kwargs["timeout"] = "short" + + _extend_list_in_kwargs_without_duplicates( + kwargs, + "tags", + [ + # QEMU networking requires TAP interfaces, which need CAP_NET_ADMIN. + # Thus have root privileges inside the sandbox. + "requires-fakeroot", + # Enforce isolated network namespaces for sandboxed tests (linux-sandbox), required by fixed network ports + "block-network", + ], + ) + + py_itf_test( + name = name, + srcs = srcs, + plugins = select({ + "@platforms//os:qnx": [ + "@score_itf//score/itf/plugins:qemu_plugin", + ], + "//quality/integration_testing/flags:linux_qemu": [ + "//quality/integration_testing/plugins/linux_qemu:linux_qemu_plugin", + ], + "//conditions:default": [], + }), + env = {"DOCKER_HOST": ""}, + **kwargs + ) diff --git a/quality/integration_testing/plugins/linux_qemu/BUILD b/quality/integration_testing/plugins/linux_qemu/BUILD new file mode 100644 index 000000000..1a089b7bd --- /dev/null +++ b/quality/integration_testing/plugins/linux_qemu/BUILD @@ -0,0 +1,55 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_python//python:defs.bzl", "py_library", "py_test") +load("@score_itf//bazel:py_itf_plugin.bzl", "py_itf_plugin") + +py_library( + name = "linux_qemu_lib", + srcs = [ + "__init__.py", + "config.py", + "qemu.py", + "qemu_process.py", + ], + imports = ["../.."], + target_compatible_with = ["@platforms//os:linux"], + visibility = ["//visibility:public"], + deps = [ + "@score_itf//score/itf/core/process", + "@score_itf//score/itf/core/utils", + "@score_itf//score/itf/plugins:core", + "@score_itf//score/itf/plugins/qemu", + "@score_someip_gateway_pip//pytest", + ], +) + +py_itf_plugin( + name = "linux_qemu_plugin", + enabled_plugins = [ + "quality.integration_testing.plugins.linux_qemu", + ], + py_library = ":linux_qemu_lib", + visibility = ["//visibility:public"], +) + +py_test( + name = "linux_qemu_unit_tests", + srcs = ["qemu_test.py"], + imports = ["../.."], + main = "qemu_test.py", + target_compatible_with = ["@platforms//os:linux"], + deps = [ + ":linux_qemu_lib", + ], +) diff --git a/quality/integration_testing/plugins/linux_qemu/README.md b/quality/integration_testing/plugins/linux_qemu/README.md new file mode 100644 index 000000000..ef881d83a --- /dev/null +++ b/quality/integration_testing/plugins/linux_qemu/README.md @@ -0,0 +1,38 @@ + + +# Linux QEMU + +For Linux QEMU, the macro provide the following arguments: + +- `--qemu-config` QEMU config following the format of ITF QEMU config +- `--qemu-image` Linux base image with [cloud-init](https://docs.cloud-init.io/en/latest/index.html) support +- `--qemu-seed-iso` Image containing [cloud-init](https://docs.cloud-init.io/en/latest/index.html) configuration +- `--qemu-filesystem-tar` built from the `filesystem` target + +The custom plugin behavior is: + +1. Create a **temporary qcow2 overlay** backed by `--qemu-image`. +2. Boot QEMU from that overlay (optionally with seed ISO). +3. Upload and extract `--qemu-filesystem-tar` into `/` inside the guest. +4. Remove the temporary overlay after the test session. + +This is intentional: the Linux base image is treated as a pristine input and is **not modified** by test execution. +That ensures that tests cannot leave artifacts influencing subsequent tests. + +## Why this plugin + +The ITF QEMU plugin does not support specifying more than one disk via `--qemu-seed-iso`, +which is needed for customization without modification of the base image. diff --git a/quality/integration_testing/plugins/linux_qemu/__init__.py b/quality/integration_testing/plugins/linux_qemu/__init__.py new file mode 100644 index 000000000..128b280ca --- /dev/null +++ b/quality/integration_testing/plugins/linux_qemu/__init__.py @@ -0,0 +1,209 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Custom QEMU plugin for Linux disk-boot integration tests. + +This plugin extends the S-CORE ITF QEMU plugin to support booting from a disk +image (qcow2) with an optional cloud-init seed ISO, without requiring patches +to the upstream score_itf package. +""" + +import logging +import os +import socket +import subprocess +import tempfile +import time + +import pytest + +from score.itf.core.utils.bunch import Bunch +from score.itf.plugins.qemu.qemu_target import QemuTarget + +from quality.integration_testing.plugins.linux_qemu.qemu_process import LinuxQemuProcess +from quality.integration_testing.plugins.linux_qemu.config import load_configuration + +logger = logging.getLogger(__name__) + + +# Cloud-init first-boot needs significantly more time than a pre-configured VM. +_SSH_BOOT_TIMEOUT = 20 +_SSH_BOOT_RETRIES = 18 +_TARGET_READY_ATTEMPTS = 8 +_TARGET_READY_BACKOFF_SECONDS = 20 + + +def _wait_for_target_ready(target): + """Wait for SSH and SFTP to become available on the target. + + Uses longer timeouts than the upstream pre_tests_phase to accommodate + cloud-init first-boot provisioning on a fresh qcow2 overlay. + """ + last_error = None + for attempt in range(1, _TARGET_READY_ATTEMPTS + 1): + try: + with target.ssh( + timeout=_SSH_BOOT_TIMEOUT, + n_retries=_SSH_BOOT_RETRIES, + retry_interval=5, + ) as ssh: + result = ssh.execute_command("echo ready") + if result != 0: + raise RuntimeError("SSH command on target failed after boot") + logger.info("Target SSH is ready") + + with target.sftp() as sftp: + result = sftp.list_dirs_and_files("/") + if not result: + raise RuntimeError("SFTP command on target failed") + logger.info("Target SFTP is ready") + return + except Exception as error: # noqa: BLE001 - external transport exceptions vary by backend. + last_error = error + if attempt >= _TARGET_READY_ATTEMPTS: + break + + backoff_seconds = attempt * _TARGET_READY_BACKOFF_SECONDS + logger.warning( + "Target is not ready yet (attempt %d/%d): %s. Retrying in %ds...", + attempt, + _TARGET_READY_ATTEMPTS, + error, + backoff_seconds, + ) + time.sleep(backoff_seconds) + + raise RuntimeError( + f"Target readiness check failed after {_TARGET_READY_ATTEMPTS} attempts: {last_error}" + ) from last_error + + +def pytest_addoption(parser): + parser.addoption( + "--qemu-config", + action="store", + required=True, + help="Path to JSON file with QEMU target configuration.", + ) + parser.addoption( + "--qemu-image", + action="store", + required=True, + help="Path to a QEMU disk image (qcow2).", + ) + parser.addoption( + "--qemu-seed-iso", + action="store", + default=None, + help="Path to a cloud-init NoCloud seed ISO.", + ) + parser.addoption( + "--qemu-filesystem-tar", + action="store", + default=None, + help="Path to a tar archive containing the test filesystem to deploy onto the QEMU target.", + ) + + +@pytest.fixture(scope="session") +def dlt(): + """Overrideable fixture for enabling DLT collection.""" + pass + + +@pytest.fixture(scope="session") +def config(request): + qemu_image = os.path.abspath(request.config.getoption("qemu_image")) + qemu_seed_iso = request.config.getoption("qemu_seed_iso") + if qemu_seed_iso: + qemu_seed_iso = os.path.abspath(qemu_seed_iso) + + return Bunch( + qemu_config=load_configuration( + os.path.abspath(request.config.getoption("qemu_config")) + ), + qemu_image=qemu_image, + qemu_seed_iso=qemu_seed_iso, + ) + + +@pytest.fixture(scope="session") +def target_init(config, request, dlt): + logger.info(f"Starting tests on host: {socket.gethostname()}") + + # Create a qcow2 overlay backed by the pristine base image so that each + # test session starts from an unmodified disk. All writes go to the + # ephemeral overlay which is discarded after the session. + base_image = config.qemu_image + overlay_fd, overlay_path = tempfile.mkstemp(suffix=".qcow2", prefix="qemu_overlay_") + os.close(overlay_fd) + try: + subprocess.run( + [ + "qemu-img", + "create", + "-f", + "qcow2", + "-b", + base_image, + "-F", + "qcow2", + overlay_path, + ], + check=True, + capture_output=True, + ) + logger.info(f"Created qcow2 overlay: {overlay_path} (backing: {base_image})") + + process = LinuxQemuProcess( + path_to_qemu_image=overlay_path, + available_ram=config.qemu_config.qemu_ram_size, + available_cores=config.qemu_config.qemu_num_cores, + network_adapters=[adapter.name for adapter in config.qemu_config.networks], + port_forwarding=config.qemu_config.port_forwarding, + seed_iso=config.qemu_seed_iso, + ) + + with process: + target = QemuTarget(process, config.qemu_config) + _wait_for_target_ready(target) + yield target + finally: + if os.path.exists(overlay_path): + os.remove(overlay_path) + logger.info(f"Removed qcow2 overlay: {overlay_path}") + + +@pytest.fixture(scope="session", autouse=True) +def deploy_filesystem(request, target_init): + """Upload and extract the test filesystem tar onto the QEMU target.""" + tar_path = request.config.getoption("qemu_filesystem_tar") + if not tar_path: + return + + tar_path = os.path.abspath(tar_path) + if not os.path.isfile(tar_path): + pytest.skip(f"Filesystem tar not found: {tar_path}") + return + + remote_tar = "/tmp/_test_filesystem.tar" + + logger.info(f"Uploading test filesystem from {tar_path} to {remote_tar}") + target_init.upload(tar_path, remote_tar) + + logger.info("Extracting test filesystem on target") + exit_code, output = target_init.execute(f"tar xf {remote_tar} -C /") + if exit_code != 0: + pytest.fail(f"Failed to extract filesystem tar on target: {output}") + + exit_code, _ = target_init.execute(f"rm -f {remote_tar}") + logger.info("Test filesystem deployed successfully") diff --git a/quality/integration_testing/plugins/linux_qemu/config.py b/quality/integration_testing/plugins/linux_qemu/config.py new file mode 100644 index 000000000..c66088e00 --- /dev/null +++ b/quality/integration_testing/plugins/linux_qemu/config.py @@ -0,0 +1,126 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""Configuration loading for the Linux QEMU plugin. + +Provides a lightweight configuration loader that does not depend on pydantic, +avoiding the need to reference the score_itf-internal pip hub. +""" + +import ipaddress +import json +import logging +import re + +logger = logging.getLogger(__name__) + +_RAM_SIZE_PATTERN = re.compile(r"^[0-9]+[KMGTP]$") + + +class PortForwarding: + __slots__ = ("host_port", "guest_port") + + def __init__(self, host_port: int, guest_port: int): + if not (1 <= host_port <= 65535): + raise ValueError(f"host_port must be 1..65535, got {host_port}") + if not (1 <= guest_port <= 65535): + raise ValueError(f"guest_port must be 1..65535, got {guest_port}") + self.host_port = host_port + self.guest_port = guest_port + + +class Network: + __slots__ = ("name", "ip_address", "gateway") + + def __init__(self, name: str, ip_address: str, gateway: str): + if not name: + raise ValueError("network name must not be empty") + ipaddress.IPv4Address(ip_address) + ipaddress.IPv4Address(gateway) + self.name = name + self.ip_address = ip_address + self.gateway = gateway + + +class QemuConfig: + """Validated QEMU configuration.""" + + __slots__ = ( + "networks", + "ssh_port", + "qemu_num_cores", + "qemu_ram_size", + "port_forwarding", + ) + + def __init__( + self, networks, ssh_port, qemu_num_cores, qemu_ram_size, port_forwarding + ): + self.networks = networks + self.ssh_port = ssh_port + self.qemu_num_cores = qemu_num_cores + self.qemu_ram_size = qemu_ram_size + self.port_forwarding = port_forwarding + + +def load_configuration(config_file: str) -> QemuConfig: + """Load and validate a QEMU configuration file. + + Args: + config_file: Path to a JSON configuration file. + + Returns: + A validated QemuConfig instance. + + Raises: + ValueError: If validation fails. + """ + logger.info(f"Loading configuration from {config_file}") + + with open(config_file, "r") as f: + data = json.load(f) + + # Networks + raw_networks = data.get("networks", []) + if not raw_networks: + raise ValueError( + f"Invalid config '{config_file}': at least one network required" + ) + networks = [Network(**n) for n in raw_networks] + + # SSH port + ssh_port = data.get("ssh_port") + if not isinstance(ssh_port, int) or not (1 <= ssh_port <= 65535): + raise ValueError(f"Invalid config '{config_file}': ssh_port must be 1..65535") + + # Cores + qemu_num_cores = data.get("qemu_num_cores") + if not isinstance(qemu_num_cores, int) or qemu_num_cores < 1: + raise ValueError(f"Invalid config '{config_file}': qemu_num_cores must be >= 1") + + # RAM + qemu_ram_size = data.get("qemu_ram_size", "") + if not _RAM_SIZE_PATTERN.match(qemu_ram_size): + raise ValueError( + f"Invalid config '{config_file}': qemu_ram_size must match [0-9]+[KMGTP]" + ) + + # Port forwarding + port_forwarding = [PortForwarding(**pf) for pf in data.get("port_forwarding", [])] + + return QemuConfig( + networks=networks, + ssh_port=ssh_port, + qemu_num_cores=qemu_num_cores, + qemu_ram_size=qemu_ram_size, + port_forwarding=port_forwarding, + ) diff --git a/quality/integration_testing/plugins/linux_qemu/qemu.py b/quality/integration_testing/plugins/linux_qemu/qemu.py new file mode 100644 index 000000000..cb4b9e4eb --- /dev/null +++ b/quality/integration_testing/plugins/linux_qemu/qemu.py @@ -0,0 +1,190 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""QEMU launcher with disk boot support. + +Provides a standalone QEMU class that supports booting from a qcow2 disk image +with an optional cloud-init seed ISO. This avoids patching the upstream +score_itf Qemu class whose private methods use Python name mangling. +""" + +import logging +import os +import subprocess +import sys +from subprocess import TimeoutExpired + +logger = logging.getLogger(__name__) + + +class DiskBootQemu: + """QEMU instance that boots from a qcow2 disk image. + + Supports an optional cloud-init seed ISO for automated guest provisioning. + Port forwarding is used for host-to-guest networking (user-mode networking). + """ + + def __init__( + self, + path_to_image, + ram="1G", + cores="2", + seed_iso=None, + cpu="Cascadelake-Server-v5", + network_adapters=None, + port_forwarding=None, + ): + self._qemu_path = "/usr/bin/qemu-system-x86_64" + self._path_to_image = path_to_image + self._ram = ram + self._cores = cores + self._seed_iso = seed_iso + self._cpu = cpu + self._network_adapters = network_adapters or [] + self._port_forwarding = port_forwarding or [] + + self._check_qemu_is_installed() + self._find_available_kvm_support() + self._check_kvm_readable_when_necessary() + + self._subprocess = None + + def __enter__(self): + return self.start() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + + def start(self, subprocess_params=None): + cmd = self._build_command() + logger.debug(cmd) + subprocess_args = {"args": cmd} + if subprocess_params: + subprocess_args.update(subprocess_params) + self._subprocess = subprocess.Popen(**subprocess_args) + return self._subprocess + + def stop(self): + if self._subprocess is None: + return + + if self._subprocess.poll() is None: + self._subprocess.terminate() + try: + self._subprocess.wait(2) + except TimeoutExpired: + logger.warning("QEMU did not terminate in time. Killing process.") + if self._subprocess.poll() is None: + self._subprocess.kill() + try: + self._subprocess.wait(2) + except TimeoutExpired: + logger.error("QEMU did not exit after kill.") + ret = self._subprocess.returncode + if ret != 0: + raise Exception(f"QEMU process returned: {ret}") + + def _check_qemu_is_installed(self): + if not os.path.isfile(self._qemu_path): + logger.fatal(f"QEMU is not installed under {self._qemu_path}") + sys.exit(-1) + + def _find_available_kvm_support(self): + self._accelerator = "kvm" + with open("/proc/cpuinfo") as cpuinfo: + cpu_options = str(cpuinfo.read()) + if "vmx" not in cpu_options and "svm" not in cpu_options: + logger.error("No virtualization capability. Using TCG accel.") + self._accelerator = "tcg" + if not os.path.exists("/dev/kvm"): + logger.error("No KVM available. Using TCG accel.") + self._accelerator = "tcg" + + def _check_kvm_readable_when_necessary(self): + if self._accelerator == "kvm" and not os.access("/dev/kvm", os.R_OK): + logger.fatal( + "No access to /dev/kvm. Consider adding yourself to kvm group." + ) + sys.exit(-1) + + def _build_command(self): + image_path = os.path.abspath(self._path_to_image) + cmd = [ + self._qemu_path, + "-smp", + f"{self._cores},maxcpus={self._cores},cores={self._cores}", + "-cpu", + self._cpu, + "-m", + self._ram, + "-drive", + f"file={image_path},format=qcow2,if=virtio", + ] + + cmd += ["-enable-kvm"] if self._accelerator == "kvm" else ["-accel", "tcg"] + + if self._seed_iso: + seed_path = os.path.abspath(self._seed_iso) + cmd.extend( + [ + # Attach NoCloud seed as a second disk. With cloud-localds this is a + # vfat image labeled 'cidata', which cloud-init detects reliably. + "-drive", + # Bazel runfiles are read-only; mounting the seed image as read-only + # prevents permission errors when QEMU opens the backing file. + f"file={seed_path},format=raw,if=virtio,readonly=on", + ] + ) + + cmd.extend( + [ + "-nographic", + "-serial", + "mon:stdio", + "-object", + "rng-random,filename=/dev/urandom,id=rng0", + "-device", + "virtio-rng-pci,rng=rng0", + ] + ) + + cmd.extend(self._network_devices_args()) + cmd.extend(self._port_forwarding_args()) + + return cmd + + def _network_devices_args(self): + result = [] + for idx, adapter in enumerate(self._network_adapters, start=1): + if not adapter.startswith("lo"): + result.extend( + [ + "-netdev", + f"tap,id=t{idx},ifname={adapter},script=no,downscript=no", + "-device", + f"virtio-net-pci,netdev=t{idx},id=nic{idx},guest_csum=off", + ] + ) + return result + + def _port_forwarding_args(self): + result = [] + for idx, forwarding in enumerate(self._port_forwarding, start=1): + result.extend( + [ + "-netdev", + f"user,id=net{idx},hostfwd=tcp::{forwarding.host_port}-:{forwarding.guest_port}", + "-device", + f"virtio-net-pci,netdev=net{idx}", + ] + ) + return result diff --git a/quality/integration_testing/plugins/linux_qemu/qemu_process.py b/quality/integration_testing/plugins/linux_qemu/qemu_process.py new file mode 100644 index 000000000..20fcc0bf7 --- /dev/null +++ b/quality/integration_testing/plugins/linux_qemu/qemu_process.py @@ -0,0 +1,79 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +"""QEMU process management with disk boot support.""" + +import logging +import subprocess + +from score.itf.core.process.console import PipeConsole +from quality.integration_testing.plugins.linux_qemu.qemu import DiskBootQemu + +logger = logging.getLogger(__name__) + + +class LinuxQemuProcess: + """Manages a QEMU process lifecycle with disk boot support.""" + + def __init__( + self, + path_to_qemu_image, + available_ram, + available_cores, + network_adapters=None, + port_forwarding=None, + seed_iso=None, + ): + self._path_to_qemu_image = path_to_qemu_image + self._available_ram = available_ram + self._available_cores = available_cores + self._network_adapters = network_adapters or [] + self._port_forwarding = port_forwarding or [] + self._qemu = DiskBootQemu( + self._path_to_qemu_image, + self._available_ram, + self._available_cores, + seed_iso=seed_iso, + network_adapters=self._network_adapters, + port_forwarding=self._port_forwarding, + ) + self._console = None + + def __enter__(self): + return self.start() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + + def start(self): + logger.info("Starting QEMU (disk boot)...") + logger.info(f"Using QEMU image: {self._path_to_qemu_image}") + subprocess_params = { + "stdin": subprocess.PIPE, + "stdout": subprocess.PIPE, + "stderr": subprocess.STDOUT, + } + qemu_subprocess = self._qemu.start(subprocess_params) + self._console = PipeConsole("QEMU", qemu_subprocess) + return self + + def stop(self): + logger.info("Stopping QEMU...") + self._qemu.stop() + + def restart(self): + self.stop() + self.start() + + @property + def console(self): + return self._console diff --git a/quality/integration_testing/plugins/linux_qemu/qemu_test.py b/quality/integration_testing/plugins/linux_qemu/qemu_test.py new file mode 100644 index 000000000..90bafe64b --- /dev/null +++ b/quality/integration_testing/plugins/linux_qemu/qemu_test.py @@ -0,0 +1,114 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import unittest +import importlib.util +import os +from pathlib import Path +from subprocess import TimeoutExpired +from unittest.mock import patch + + +def _load_qemu_module(): + module_path = Path(__file__).with_name("qemu.py") + spec = importlib.util.spec_from_file_location("linux_qemu_qemu", module_path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +DiskBootQemu = _load_qemu_module().DiskBootQemu + + +class DiskBootQemuTest(unittest.TestCase): + def _new_qemu(self, seed_iso=None): + with ( + patch.object(DiskBootQemu, "_check_qemu_is_installed"), + patch.object(DiskBootQemu, "_find_available_kvm_support"), + patch.object(DiskBootQemu, "_check_kvm_readable_when_necessary"), + ): + qemu = DiskBootQemu( + path_to_image="/tmp/image.qcow2", + seed_iso=seed_iso, + ) + qemu._accelerator = "tcg" + return qemu + + def test_build_command_adds_seed_iso_as_drive(self): + qemu = self._new_qemu(seed_iso="/tmp/seed.iso") + + cmd = qemu._build_command() + + self.assertIn("-drive", cmd) + self.assertIn("file=/tmp/seed.iso,format=raw,if=virtio,readonly=on", cmd) + + def test_build_command_without_seed_iso_omits_seed_drive(self): + qemu = self._new_qemu(seed_iso=None) + + cmd = qemu._build_command() + + self.assertNotIn("file=/tmp/seed.iso,format=raw,if=virtio,readonly=on", cmd) + + def test_build_command_normalizes_relative_seed_iso_to_absolute(self): + qemu = self._new_qemu( + seed_iso="quality/integration_testing/environments/ubuntu24_04_qemu/seed.img" + ) + + cmd = qemu._build_command() + + expected = os.path.abspath( + "quality/integration_testing/environments/ubuntu24_04_qemu/seed.img" + ) + self.assertIn(f"file={expected},format=raw,if=virtio,readonly=on", cmd) + + def test_build_command_uses_valid_tcg_acceleration_args(self): + qemu = self._new_qemu(seed_iso=None) + qemu._accelerator = "tcg" + + cmd = qemu._build_command() + + self.assertIn("-accel", cmd) + accel_pos = cmd.index("-accel") + self.assertEqual(cmd[accel_pos + 1], "tcg") + + def test_build_command_uses_enable_kvm_flag(self): + qemu = self._new_qemu(seed_iso=None) + qemu._accelerator = "kvm" + + cmd = qemu._build_command() + + self.assertIn("-enable-kvm", cmd) + + def test_stop_handles_unstarted_process(self): + qemu = self._new_qemu(seed_iso=None) + + qemu.stop() + + def test_stop_falls_back_to_kill_after_terminate_timeout(self): + qemu = self._new_qemu(seed_iso=None) + + process = unittest.mock.MagicMock() + process.poll.side_effect = [None, None, 0] + process.wait.side_effect = [TimeoutExpired(cmd="qemu", timeout=2), 0] + process.returncode = 0 + qemu._subprocess = process + + qemu.stop() + + process.terminate.assert_called_once() + process.kill.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/quality/integration_testing/requirements.txt b/quality/integration_testing/requirements.txt new file mode 100644 index 000000000..a10fbcc3e --- /dev/null +++ b/quality/integration_testing/requirements.txt @@ -0,0 +1,15 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +bazel-runfiles==1.8.4 +docker==7.1.0 +pytest==8.4.1 diff --git a/quality/integration_testing/test/BUILD b/quality/integration_testing/test/BUILD new file mode 100644 index 000000000..c0537ac75 --- /dev/null +++ b/quality/integration_testing/test/BUILD @@ -0,0 +1,46 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("//quality/integration_testing:integration_testing.bzl", "integration_test") + +cc_binary( + name = "example-app", + srcs = ["main.cpp"], +) + +cc_binary( + name = "example-app2", + srcs = ["main2.cpp"], +) + +pkg_files( + name = "example-app-pkg", + srcs = [ + ":example-app", + ":example-app2", + ], + attributes = pkg_attributes( + mode = "0777", + ), + prefix = "/", +) + +integration_test( + name = "test_hello_world", + timeout = "moderate", + srcs = [ + "test_hello_world.py", + ], + filesystem = ":example-app-pkg", +) diff --git a/quality/integration_testing/test/main.cpp b/quality/integration_testing/test/main.cpp new file mode 100644 index 000000000..5ce7b1bd8 --- /dev/null +++ b/quality/integration_testing/test/main.cpp @@ -0,0 +1,22 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +#include +#include + +int main() +{ + std::cout << "Hello!\n"; + // We create a file to signal to `main2.cpp` that it can continue. + std::ofstream{"/tmp/Hello.txt"}; + return 0; +} diff --git a/quality/integration_testing/test/main2.cpp b/quality/integration_testing/test/main2.cpp new file mode 100644 index 000000000..b914d6156 --- /dev/null +++ b/quality/integration_testing/test/main2.cpp @@ -0,0 +1,25 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +#include +#include + +int main() +{ + std::ifstream file("/tmp/Hello.txt"); + while (not file.is_open()) + { + file = std::ifstream("/tmp/Hello.txt"); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + return 0; +} diff --git a/quality/integration_testing/test/test_hello_world.py b/quality/integration_testing/test/test_hello_world.py new file mode 100644 index 000000000..4d3a19c39 --- /dev/null +++ b/quality/integration_testing/test/test_hello_world.py @@ -0,0 +1,32 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +def test_hello_world_via_shell(target): + exit_code, output = target.execute("echo Hello World!") + assert 0 == exit_code + assert b"Hello World!" in output + + +def test_hello_world_via_binary_execution(target): + exit_code, output = target.execute("/example-app") + assert 0 == exit_code + assert b"Hello!" in output + + +def test_hello_world_as_process(target): + with target.wrap_exec("/example-app2"): + with target.wrap_exec("/example-app", wait_on_exit=True): + pass + + +def test_ping(target): + assert target.ping(timeout=4) diff --git a/src/someipd/main.cpp b/src/someipd/main.cpp index 139a2186c..e3e3262df 100644 --- a/src/someipd/main.cpp +++ b/src/someipd/main.cpp @@ -14,11 +14,13 @@ #include #include +#include #include #include #include #include #include +#include #include "routing.h" #include "score/filesystem/path.h" @@ -102,7 +104,7 @@ int main(int argc, char* argv[]) { // Both configurations are required, otherwise print help and exit if (configuration_path.Empty() || service_instance_manifest_path.Empty()) { print_help(); - return 1; + return EXIT_FAILURE; } // Read config data @@ -112,16 +114,17 @@ int main(int argc, char* argv[]) { if (!config_file.is_open()) { score::mw::log::LogFatal() << "Error: Could not open config file " << configuration_path; - return 1; + return EXIT_FAILURE; } config_file.seekg(0, std::ios::end); std::streampos length = config_file.tellg(); if (length <= 0) { - score::mw::log::LogFatal() << "Error: Invalid config file size: " << static_cast(length); + score::mw::log::LogFatal() + << "Error: Invalid config file size: " << static_cast(length); config_file.close(); - return 1; + return EXIT_FAILURE; } config_file.seekg(0, std::ios::beg); @@ -135,11 +138,31 @@ int main(int argc, char* argv[]) { score::mw::com::runtime::InitializeRuntime( score::mw::com::runtime::RuntimeConfiguration{service_instance_manifest_path}); - auto handles = - SomeipMessageTransferProxy::FindService( + std::vector handles; + + while (handles.empty() && !shutdown_requested.load()) { + auto find_result = SomeipMessageTransferProxy::FindService( score::mw::com::InstanceSpecifier::Create(std::string("someipd/gatewayd_messages")) - .value()) - .value(); + .value()); + + if (!find_result.has_value()) { + std::cerr << "[someipd] Error finding service: " << find_result.error().Message() + << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + + handles = find_result.value(); + + if (handles.empty()) { + std::cout << "[someipd] Waiting for gatewayd to start..." << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + + if (shutdown_requested.load()) { + return EXIT_SUCCESS; + } // Proxy for receiving messages from gatewayd to be sent via SOME/IP auto proxy = SomeipMessageTransferProxy::Create(handles.front()).value(); @@ -165,4 +188,5 @@ int main(int argc, char* argv[]) { routing.value().Run(shutdown_requested); std::cout << "[someipd] Shutting down SOME/IP daemon..." << std::endl; + return EXIT_SUCCESS; } diff --git a/tests/integration_test/BUILD b/tests/integration_test/BUILD new file mode 100644 index 000000000..da6864577 --- /dev/null +++ b/tests/integration_test/BUILD @@ -0,0 +1,127 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +""" +Integration Tests +""" + +load("@rules_pkg//pkg:mappings.bzl", "pkg_attributes", "pkg_files") +load("//quality/integration_testing:integration_testing.bzl", "integration_test") + +pkg_files( + name = "vsomeip-runtime-pkg", + srcs = [ + "@vsomeip", + ], + attributes = pkg_attributes( + mode = "0777", + ), + prefix = "/usr", +) + +pkg_files( + name = "example-app-pkg", + srcs = [ + ":vsomeip.json", + ":vsomeip-runtime-pkg", + "//src/config:config_file", + "//src/gatewayd", + "//src/gatewayd:etc/mw_com_config.json", + "//src/serializer:null_serializer", + "//src/someipd", + "//src/someipd:etc/mw_com_config.json", + "//tests/integration/sample_client", + ], + attributes = pkg_attributes( + mode = "0777", + ), + prefix = "/", + renames = { + "//src/config:config_file": "mw_someip_config.bin", + "//src/someipd:etc/mw_com_config.json": "someipd_mw_com_config.json", + "//src/gatewayd:etc/mw_com_config.json": "gatewayd_mw_com_config.json", + }, +) + +integration_test( + name = "test_tcpdump", + timeout = "moderate", + srcs = [ + "tcpdump.py", + "util.py", + ], + filesystem = ":example-app-pkg", + deps = [ + "@score_someip_gateway_pip//pytest", + ], +) + +integration_test( + name = "test_gateway_startup", + timeout = "moderate", + srcs = [ + "conftest.py", + "gatewayd_startup.py", + "util.py", + ], + filesystem = ":example-app-pkg", + deps = [ + "@score_someip_gateway_pip//pytest", + "@score_someip_gateway_pip//someip", + ], +) + +integration_test( + name = "test_someipd_startup", + timeout = "moderate", + srcs = [ + "conftest.py", + "someipd_startup.py", + "util.py", + ], + filesystem = ":example-app-pkg", + deps = [ + "@score_someip_gateway_pip//pytest", + "@score_someip_gateway_pip//someip", + ], +) + +integration_test( + name = "test_someipd_sends_someip-sd_message", + timeout = "moderate", + srcs = [ + "conftest.py", + "someipd_sends_someip-sd_message.py", + "util.py", + ], + filesystem = ":example-app-pkg", + deps = [ + "@score_someip_gateway_pip//pytest", + "@score_someip_gateway_pip//someip", + ], +) + +integration_test( + name = "test_someipd_started_by_test", + timeout = "moderate", + srcs = [ + "conftest.py", + "someipd_started_by_test.py", + "util.py", + ], + filesystem = ":example-app-pkg", + deps = [ + "@score_someip_gateway_pip//pytest", + "@score_someip_gateway_pip//someip", + ], +) diff --git a/tests/integration_test/README.md b/tests/integration_test/README.md new file mode 100644 index 000000000..fd87b48ef --- /dev/null +++ b/tests/integration_test/README.md @@ -0,0 +1,50 @@ + + +# Integration Tests + +Integration tests for testing network behavior of the SOME/IP Gateway. + +## Network trace + +For each test, a `.pcap` file is recorded in the test output directory, which contains the network traffic during the test execution. This can be used for debugging and verification of the expected network interactions. + +> [!NOTE] +> For whatever reason `tcpdump` cannot be killed at any time and you may get a permission denied error. +> Thus it is terminated by terminating `linux-sandbox`. + +## How to use + +Each test must use the `clean_state` fixture or use one based on that. +The `clean_state` fixture checks that the environment has not been used by a previous tests and fails if it detects leftover state. +This ensures that tests do not interfere with each other and that the test environment is clean for each test. + +```python +def test_example(clean_state): + # test code here +``` + +This implies that there is only one `test_*` function per test file and only one test file per Bazel target. + +```bazel +integration_test( + name = "test_example", + srcs = [ + "conftest.py", # contains test fixture clean_state + "test_example.py", + ], + ... +) +``` diff --git a/tests/integration_test/conftest.py b/tests/integration_test/conftest.py new file mode 100644 index 000000000..a204a6466 --- /dev/null +++ b/tests/integration_test/conftest.py @@ -0,0 +1,86 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + + +""" +Pytest configuration and fixtures for integration tests. +""" + +import os +import pytest +from typing import Generator +from util import ShellProcess, tcpdump_capture, check_environment_and_mark +from score.itf.plugins.core import Target + + +@pytest.fixture(scope="function") +def clean_state(target: Target) -> Generator[Target, None, None]: + """This fixture can only be used once per QEMU instance / linux-sandbox instance, to ensure a clean environment. + + Otherwise subsequent tests might fail due to leftover state from previous tests. + Additionally killing tcpdump is problematic (kill errors with permission denied), but terminating linux-sandbox should get rid of it. + Lastly the output pcap file name should contain the test name, if we want to support multiple tests per run. + """ + + check_environment_and_mark(target) + yield target + + +@pytest.fixture(scope="function") +def gatewayd_with_someipd(clean_state: Target) -> Generator[Target, None, None]: + """Start someipd and gatewayd before tests and stop them after.""" + + # TODO tcpdump cannot be killed when the test is done. Need to figure out why + + # Store test traffic in file for later analysis of failures + pcap_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR", ".") + pcap_file = os.path.join(pcap_dir, "test_traffic.pcap") + + # no with statement is used by intention to avoid permission denied error when killing tcpdump + tcpdump_host = tcpdump_capture("", output_file=pcap_file) + tcpdump_process = tcpdump_host.__enter__() + + with ShellProcess( + clean_state, + "/someipd", + args=[ + "--configuration", + "/mw_someip_config.bin", + "--service_instance_manifest", + "/someipd_mw_com_config.json", + ], + env="VSOMEIP_CONFIGURATION=/vsomeip.json", + ) as someipd_process: + assert someipd_process.is_running(), someipd_process.get_output() + with ShellProcess( + clean_state, + "/gatewayd", + args=[ + "--configuration", + "/mw_someip_config.bin", + "--service_instance_manifest", + "/gatewayd_mw_com_config.json", + ], + ) as gatewayd_process: + assert gatewayd_process.is_running(), gatewayd_process.get_output() + assert gatewayd_process.is_running(), ( + gatewayd_process.get_output(), + "exit code: ", + gatewayd_process.get_exit_code(), + ) + assert someipd_process.is_running(), ( + someipd_process.get_output(), + "exit code: ", + someipd_process.get_exit_code(), + ) + yield clean_state diff --git a/tests/integration_test/gatewayd_startup.py b/tests/integration_test/gatewayd_startup.py new file mode 100644 index 000000000..5cdaffe5a --- /dev/null +++ b/tests/integration_test/gatewayd_startup.py @@ -0,0 +1,45 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import logging +import time +from util import ( + ShellProcess, + get_running_processes_on_target, +) + + +def test_start_gatewayd(clean_state): + with ShellProcess( + clean_state, + "/gatewayd", + args=[ + "--configuration", + "/mw_someip_config.bin", + "--service_instance_manifest", + "/gatewayd_mw_com_config.json", + ], + ) as gatewayd_process: + assert gatewayd_process.is_running(), gatewayd_process.get_output() + time.sleep(1) # check that daemon does not crash immediately and prints output + assert gatewayd_process.is_running(), ( + gatewayd_process.get_output(), + "exit code: ", + gatewayd_process.get_exit_code(), + ) + logging.info("gatewayd output:\n%s", gatewayd_process.get_output()) + ps_aux_text = get_running_processes_on_target(clean_state) + assert "gatewayd" in ps_aux_text, ps_aux_text + + ps_aux_text = get_running_processes_on_target(clean_state) + assert "gatewayd" not in ps_aux_text, ps_aux_text diff --git a/tests/integration_test/someipd_sends_someip-sd_message.py b/tests/integration_test/someipd_sends_someip-sd_message.py new file mode 100644 index 000000000..339ad65a9 --- /dev/null +++ b/tests/integration_test/someipd_sends_someip-sd_message.py @@ -0,0 +1,28 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import logging +from util import ( + tcpdump_capture, + wait_until_process_exits, +) +from score.itf.plugins.core import Target + + +def test_start_someipd_and_gatewayd(gatewayd_with_someipd: Target) -> None: + """Same as above but with more complex test fixture""" + with tcpdump_capture("udp port 30490", packet_count=1) as tcpdump_process: + console_output = wait_until_process_exits(tcpdump_process, timeout=10.0) + logging.info( + "Final tcpdump to capture SOME/IP-SD traffic...\n" + console_output + ) diff --git a/tests/integration_test/someipd_started_by_test.py b/tests/integration_test/someipd_started_by_test.py new file mode 100644 index 000000000..45a15eb2d --- /dev/null +++ b/tests/integration_test/someipd_started_by_test.py @@ -0,0 +1,63 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import logging +from util import ( + ShellProcess, + tcpdump_capture, + wait_until_process_exits, +) +from score.itf.plugins.core import Target + + +def test_start_someipd_and_gatewayd(clean_state: Target) -> None: + """Test reception of SOME/IP-SD message from someipd in tcpdump""" + with tcpdump_capture("udp port 30490", packet_count=1) as tcpdump_process: + with ShellProcess( + clean_state, + "/someipd", + args=[ + "--configuration", + "/mw_someip_config.bin", + "--service_instance_manifest", + "/someipd_mw_com_config.json", + ], + env="VSOMEIP_CONFIGURATION=/vsomeip.json", + ) as someipd_process: + assert someipd_process.is_running(), someipd_process.get_output() + with ShellProcess( + clean_state, + "/gatewayd", + args=[ + "--configuration", + "/mw_someip_config.bin", + "--service_instance_manifest", + "/gatewayd_mw_com_config.json", + ], + ) as gatewayd_process: + assert gatewayd_process.is_running(), gatewayd_process.get_output() + console_output = wait_until_process_exits(tcpdump_process, timeout=10.0) + logging.info( + "Final tcpdump to capture SOME/IP-SD traffic...\n" + console_output + ) + + assert gatewayd_process.is_running(), ( + gatewayd_process.get_output(), + "exit code: ", + gatewayd_process.get_exit_code(), + ) + assert someipd_process.is_running(), ( + someipd_process.get_output(), + "exit code: ", + someipd_process.get_exit_code(), + ) diff --git a/tests/integration_test/someipd_startup.py b/tests/integration_test/someipd_startup.py new file mode 100644 index 000000000..3d65a8e56 --- /dev/null +++ b/tests/integration_test/someipd_startup.py @@ -0,0 +1,43 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import logging +import time +from util import ShellProcess, get_running_processes_on_target + + +def test_start_someipd(clean_state): + with ShellProcess( + clean_state, + "/someipd", + args=[ + "--configuration", + "/mw_someip_config.bin", + "--service_instance_manifest", + "/someipd_mw_com_config.json", + ], + env="VSOMEIP_CONFIGURATION=/vsomeip.json", + ) as someipd_process: + assert someipd_process.is_running(), someipd_process.get_output() + time.sleep(1) # check that daemon does not crash immediately and prints output + assert someipd_process.is_running(), ( + someipd_process.get_output(), + "exit code: ", + someipd_process.get_exit_code(), + ) + logging.info("someipd output:\n%s", someipd_process.get_output()) + ps_aux_text = get_running_processes_on_target(clean_state) + assert "someipd" in ps_aux_text, ps_aux_text + + ps_aux_text = get_running_processes_on_target(clean_state) + assert "someipd" not in ps_aux_text, ps_aux_text diff --git a/tests/integration_test/tcpdump.py b/tests/integration_test/tcpdump.py new file mode 100644 index 000000000..bbd6f5564 --- /dev/null +++ b/tests/integration_test/tcpdump.py @@ -0,0 +1,155 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +import logging +import time +from util import ( + ShellProcess, + tcpdump_capture, + is_tcpdump_running, + get_output, +) + + +def test_tcpdump_with_ping_from_target_execute(target) -> None: + with tcpdump_capture("icmp", packet_count=2) as tcpdump_process: + assert tcpdump_process.poll() is None, get_output(tcpdump_process) + + # sanity check that tcpdump is running + tcpdump_running, ps_aux_text = is_tcpdump_running() + assert tcpdump_running, ps_aux_text + + # It looks like tcpdump is not always ready to capture packets immediately after starting + for _ in range(5): + exit_code, output = target.execute("ping -c 1 169.254.21.88") + assert exit_code == 0, output.decode() + + # Now tcpdump should terminate with two captured packets + try: + tcpdump_process.wait(timeout=1.0) + break + except Exception as e: + logging.getLogger().error( + "Exception occurred while waiting for tcpdump to terminate: " + + str(e) + ) + assert tcpdump_process.returncode == 0, get_output(tcpdump_process) + assert tcpdump_process.poll() is not None, ( + "tcpdump process should have exited by now: " + get_output(tcpdump_process) + ) + + tcpdump_running, ps_aux_text = is_tcpdump_running() + assert not tcpdump_running, ps_aux_text + + +def test_tcpdump_with_ping_from_target(target): + with tcpdump_capture("icmp", packet_count=2) as tcpdump_process: + assert tcpdump_process.poll() is None, get_output(tcpdump_process) + + # sanity check that tcpdump is running + tcpdump_running, ps_aux_text = is_tcpdump_running() + assert tcpdump_running, ps_aux_text + + with ShellProcess(target, "ping", ["-c", "5", "169.254.21.88"]) as bash_process: + # Now tcpdump should terminate with two captured packets + tcpdump_process.wait(timeout=5.0) + assert tcpdump_process.returncode == 0, get_output(tcpdump_process) + assert tcpdump_process.poll() is not None, ( + "tcpdump process should have exited by now: " + get_output(tcpdump_process) + ) + + tcpdump_running, ps_aux_text = is_tcpdump_running() + assert not tcpdump_running, ps_aux_text + + +def test_tcpdump_with_long_running_ping_from_target(target): + with tcpdump_capture("icmp", packet_count=5) as tcpdump_process: + assert tcpdump_process.poll() is None, get_output(tcpdump_process) + + # sanity check that tcpdump is running + tcpdump_running, ps_aux_text = is_tcpdump_running() + assert tcpdump_running, ps_aux_text + + try: + with ShellProcess(target, "ping", ["169.254.21.88"]) as bash_process: + logging.getLogger().info( + "Started ping process with PID: " + str(bash_process.pid()) + ) + # sanity check that tcpdump is running + tcpdump_running, ps_aux_text = is_tcpdump_running() + assert tcpdump_running, ps_aux_text + while tcpdump_process.poll() is None: + time.sleep(0.1) + + logging.getLogger().info( + "final iteration" + get_output(tcpdump_process) + ) + + assert tcpdump_process.returncode == 0, get_output(tcpdump_process) + assert bash_process.is_running(), ( + "ping process should still be running after tcpdump has exited: " + + bash_process.get_output().decode() + ) + except Exception as e: + logging.getLogger().error( + "Exception occurred during ping process: " + str(e) + ) + raise + + logging.getLogger().info( + "Exited ping process, now waiting for tcpdump to terminate with captured packets" + ) + + # Now tcpdump should terminate with two captured packets + tcpdump_process.wait(timeout=5.0) + assert tcpdump_process.returncode == 0, get_output(tcpdump_process) + assert tcpdump_process.poll() is not None, ( + "tcpdump process should have exited by now: " + get_output(tcpdump_process) + ) + + tcpdump_running, ps_aux_text = is_tcpdump_running() + assert not tcpdump_running, ps_aux_text + + logging.getLogger().info( + "Finished test_tcpdump_with_long_running_ping_from_target2" + ) + + +def test_killing_tcpdump(target): + with tcpdump_capture("icmp", packet_count=500) as tcpdump_process: + assert tcpdump_process.poll() is None, get_output(tcpdump_process) + # killing tcpdump via exiting the with statement seems to work + # What does not work is killing it via using any of these: + # tcpdump_process.terminate() + # tcpdump_process.kill() + # subprocess.run(["pkill", "tcpdump"], check=True) + # This raises an permission exception. + + tcpdump_running, ps_aux_text = is_tcpdump_running() + assert not tcpdump_running, ps_aux_text + + +def test_killing_tcpdump_while_ping_is_running(target): + with tcpdump_capture("icmp", packet_count=500) as tcpdump_process: + assert tcpdump_process.poll() is None, get_output(tcpdump_process) + bash_process = ShellProcess(target, "ping", ["169.254.21.88"]) + ping_process = bash_process.__enter__() + assert ping_process is not None, "Failed to start ping process" + assert ping_process.is_running(), bash_process.get_output().decode() + assert tcpdump_process.poll() is None, get_output(tcpdump_process) + + tcpdump_running, ps_aux_text = is_tcpdump_running() + assert not tcpdump_running, ps_aux_text + assert ping_process.is_running(), bash_process.get_output().decode() + bash_process.__exit__(None, None, None) + assert not ping_process.is_running(), bash_process.get_output().decode() diff --git a/tests/integration_test/util.py b/tests/integration_test/util.py new file mode 100644 index 000000000..287cf6df7 --- /dev/null +++ b/tests/integration_test/util.py @@ -0,0 +1,217 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +from collections.abc import Sequence +import io +import os +import pwd +import logging +import time +import subprocess +from score.itf.plugins.core import Target +from types import TracebackType +from typing import Any +from score.itf.core.process.async_process import AsyncProcess + + +def _as_text(output: Any) -> str: + if isinstance(output, bytes): + return output.decode(errors="replace") + return str(output) + + +def _completed_process_as_text(process: subprocess.CompletedProcess) -> str: + return ( + f"Command: {' '.join(process.args)}\n" + f"Exit code: {process.returncode}\n" + f"Stdout:\n{_as_text(process.stdout)}\n" + f"Stderr:\n{_as_text(process.stderr)}" + ) + + +def _get_content_of_file_object(file_object: io.BufferedReader | None) -> str: + if file_object is None: + return "" + + # enable non blocking io + os.set_blocking(file_object.fileno(), False) + + # Read and discard any buffered content to get the latest output + data = file_object.read() + if data is None: + return "" + return data.decode(errors="replace") + + +def get_output(process: subprocess.Popen[bytes]) -> str: + return ( + _get_content_of_file_object(process.stdout) + + "\n, stderr: " + + _get_content_of_file_object(process.stderr) + ) + + +class ShellProcess: + """Similar to WrappedProcess, but allows setting environment variables.""" + + def __init__( + self, + target: Target, + application_path: str, + args: Sequence[str] | None = None, + env: str = "", + ): + self._target = target + self._application_path = application_path + self._env = env + self._args = list(args) if args is not None else [] + self._process: AsyncProcess | None = None + + def __enter__(self) -> AsyncProcess: + args = " ".join(self._args) + command = f"LD_LIBRARY_PATH=/ {self._env} exec {self._application_path} {args}" + self._process = self._target.execute_async(command) + + logging.getLogger().info( + f"Started process {self._application_path} with PID: {self._process.pid()}" + ) + + return self._process + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + if self._process is not None: + self._process.stop() + + +def tcpdump_capture( + filter_expression: str, + packet_count: int | None = None, + output_file: str | None = None, +) -> subprocess.Popen[bytes]: + tcpdump_user = pwd.getpwuid(os.getuid()).pw_name + args = [ + "/usr/bin/tcpdump", + "-n", + "-i", + "any", + "-Z", + tcpdump_user, + ] + if output_file is not None: + args.extend(["-w", output_file]) + else: + # -l: line-buffered output, only meaningful for text (non-pcap) mode + args.append("-l") + # TODO tcpdump cannot be killed, thus at the moment only packet_count can be used to stop it + # When testing it using `linux-sandbox -R -N -- /bin/bash -lc 'tcpdump -n -l -Z root -i any'` + # and `sudo nsenter -t $(pidof tcpdump) -a killall tcpdump` it is killable in the second shell + if packet_count is not None: + args.extend(["-c", str(packet_count)]) + if filter_expression: + args.append(filter_expression) + + return subprocess.Popen( + args, + stdout=subprocess.PIPE if output_file is None else subprocess.DEVNULL, + stderr=subprocess.PIPE, + ) + + +def wait_until_process_exits( + process: subprocess.Popen[bytes], timeout: float = 10.0 +) -> str: + start_time = time.time() + while time.time() - start_time < timeout: + if process.poll() is not None: + return get_output(process) + time.sleep(0.5) + raise TimeoutError( + f"Process did not exit within {timeout} seconds. Last output: {get_output(process)}" + ) + + +def get_running_processes_on_host() -> str: + ps_aux_result = subprocess.run( + ["ps", "aux"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + assert ps_aux_result.returncode == 0 + return _completed_process_as_text(ps_aux_result) + + +def get_running_processes_on_target(target) -> str: + exit_code, output = target.execute("ps aux || pidin") + assert exit_code == 0, output.decode() + return output.decode() + + +def is_tcpdump_running() -> tuple[bool, str]: + tcpdump_name = "/usr/bin/tcpdump" + # do not know why on Github runners tcpdump shows up like that + tcpdump_name_github = "[tcpdump]" + ps_aux_text = get_running_processes_on_host() + + return ( + tcpdump_name in ps_aux_text or tcpdump_name_github in ps_aux_text, + ps_aux_text, + ) + + +def check_environment_and_mark(target: Target) -> bool: + """Check that environment is clean and place marker to fail if not.""" + marker_name = "/tmp/inc_someip_gateway_test_run" + message = "Please ensure that there is only one test per file and one file per bazel label." + + # Check for marker files + host_result = subprocess.run( + ["ls", marker_name], check=False, stdout=subprocess.PIPE + ) + assert host_result.returncode != 0, ( + f"Marker file {marker_name} exists on host, environment is not clean. {message}" + ) + + return_code, output = target.execute(f"ls {marker_name}") + assert return_code != 0, ( + f"Marker file {marker_name} exists on target, environment is not clean. {message}" + ) + + # Check for running processes + ps_aux_text = get_running_processes_on_target(target) + assert "someipd" not in ps_aux_text, "Found stale someipd running: " + ps_aux_text + assert "gatewayd" not in ps_aux_text, "Found stale gatewayd running: " + ps_aux_text + + # Check for lola files Linux + QNX + for pattern in ["/dev/shm/lola-*", "/tmp/mw_com_lola/*", "/tmp/lola-*"] + [ + "/dev/shmem/lola-*", + "/tmp_discovery/mw_com_lola/*", + "/tmp_discovery/lola-*", + ]: + return_code, output = target.execute(f"ls {pattern}") + assert return_code != 0, ( + f"Found stale lola files in {pattern} on target: " + output.decode() + ) + + # Check for tcpdump running on host + is_running, ps_aux_text = is_tcpdump_running() + assert not is_running, "Found stale tcpdump running on host: " + ps_aux_text + + # create marker file to mark that the environment is now dirty + subprocess.run(["touch", marker_name], check=True) + return_code, output = target.execute(f"touch {marker_name}") + assert return_code == 0, ( + f"Failed to create marker file {marker_name} on target. Output: {output.decode()}" + ) diff --git a/tests/integration_test/vsomeip.json b/tests/integration_test/vsomeip.json new file mode 100644 index 000000000..77444a64a --- /dev/null +++ b/tests/integration_test/vsomeip.json @@ -0,0 +1,53 @@ +{ + "unicast": "169.254.158.190", + "logging": { + "level": "debug", + "console": "true", + "file": { + "enable": "false", + "path": "/var/log/vsomeip.log" + }, + "dlt": "false" + }, + "applications": [ + { + "name": "someipd", + "id": "0x0100" + } + ], + "services": [ + { + "service": "0x1234", + "instance": "0x5678", + "unreliable": "31000", + "events": [ + { + "event": "0x8778", + "is_field": "false" + } + ], + "eventgroups": [ + { + "eventgroup": "0x8778", + "events": [ + "0x8778" + ] + } + ] + } + ], + "routing": "someipd", + "service-discovery": { + "enable": "true", + "multicast": "224.244.224.245", + "port": "30490", + "protocol": "udp", + "initial_delay_min": "10", + "initial_delay_max": "100", + "repetitions_base_delay": "200", + "repetitions_max": "3", + "ttl": "3", + "cyclic_offer_delay": "2000", + "request_response_delay": "1500" + } +} diff --git a/third_party/vsomeip-qnx8.patch b/third_party/vsomeip-qnx8.patch index 7c9b84030..e0285650a 100644 --- a/third_party/vsomeip-qnx8.patch +++ b/third_party/vsomeip-qnx8.patch @@ -57,3 +57,35 @@ diff --git a/implementation/routing/src/routing_manager_impl.cpp b/implementatio #endif if (routing_ready_handler_) { +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 770ef56b..3df22b9c 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -416,7 +416,8 @@ set(EXAMPLE_CONFIG_FILES + # Configuration parameters + ################################################################################ + if(${CMAKE_SYSTEM_NAME} MATCHES "QNX") +- set (VSOMEIP_BASE_PATH "/var") ++ # /var is not writable in the QNX image ++ set (VSOMEIP_BASE_PATH "/tmp") + else() + set (VSOMEIP_BASE_PATH "/tmp") + endif() +diff --git a/implementation/utility/src/utility.cpp b/implementation/utility/src/utility.cpp +index ef7a2889..6c340ecb 100644 +--- a/implementation/utility/src/utility.cpp ++++ b/implementation/utility/src/utility.cpp +@@ -172,7 +172,12 @@ bool utility::is_routing_manager(const std::string& _network) { + VSOMEIP_ERROR << __func__ << ": Could not open " << its_lockfile << ": errno " << errno; + } + +- return (its_lock_ctrl != -1); ++ // TODO Many functions are stubbed on the QNX image. ++ // Will have to eventually figure out what needs to be done to get file locking working. ++ // This fails: fcntl(r.first->second.lock_fd_, F_SETLK, &its_lock_data) ++ VSOMEIP_WARNING << __func__ << ": Lock file \"" << its_lockfile << "\" " << ((its_lock_ctrl != -1) ? "is locked" : "locking failed") ++ << ". error: " << errno << " (" << strerror(errno) << ")"; ++ return true; + #endif + } + \ No newline at end of file