Standalone Bazel module for running C++ and Rust unit tests inside QEMU micro-virtual machines on QNX 8 (x86_64 and aarch64).
qnx_unit_tests/
├── .bazelrc # Bazel config (host + qnx-x86_64/aarch64 cross-compilation)
├── .bazelversion # Pinned Bazel version (8.6.0)
├── MODULE.bazel # Bzlmod dependencies (QCC, Ferrocene, IFS, googletest)
├── BUILD # Copyright checker and formatting targets
├── defs.bzl # Public API (cc_test_qnx, rust_test_qnx)
├── src/
│ ├── test_qnx.bzl # Core macro wrapping cc_test/rust_test for QNX microvm execution
│ ├── runfiles_manifest.bzl # Bazel rule to list runfiles (for --run_under mode)
│ ├── x86_64_qnx8/ # x86_64 QNX 8 specific files
│ │ ├── init.build.template
│ │ ├── run_qemu.sh
│ │ ├── run_qemu_shell.sh
│ │ ├── run_under_qnx.sh
│ │ ├── startup.sh
│ │ └── tools.build
│ ├── arm64_qnx8/ # aarch64 QNX 8 specific files
│ │ ├── init.build.template
│ │ ├── run_qemu.sh
│ │ ├── run_qemu_shell.sh
│ │ ├── run_under_qnx.sh
│ │ ├── startup.sh
│ │ └── tools.build
│ └── common/ # Shared scripts and drivers
│ ├── prepare_test.sh
│ ├── run_test.sh
│ └── virtio9p/ # 9P2000.L resource manager for host-guest file sharing
├── third_party/
│ └── BUILD # Stubs for QNX system libraries (libslog2, libpci)
├── tools/
│ └── qnx_credential_helper.py # Bazel credential helper for qnx.com
├── examples/ # Standalone Bazel module demonstrating external usage
│ └── MODULE.bazel
└── test/ # Example tests
├── BUILD
├── data.txt
├── main.cpp # C++ (GTest) example
└── main_rust.rs # Rust example
- Bazel 8.6.0 (via Bazelisk)
- QEMU (
qemu-system-x86_64and/orqemu-system-aarch64) - KVM access (optional, but strongly recommended for performance)
- QNX SDP 8.0 credentials (for toolchain download)
The test_qnx macro (and its aliases cc_test_qnx / rust_test_qnx) wraps a standard cc_test or rust_test target for execution inside a QEMU microvm running QNX:
- The test binary and its runfiles are packaged into a tar archive
- An IFS boot image is built containing the QNX kernel, startup scripts, and the virtio-9p driver
- QEMU boots the IFS image, mounts the test archive via virtio-9p, and executes the test
- Test results (XML, coverage) are extracted from the shared directory after execution
The .bazelrc file includes QNX-specific configs (qnx-x86_64, qnx-aarch64) that enable the necessary toolchains and flags. Of particular importance is the --experimental_retain_test_configuration_across_testonly flag, which is required for proper test extraction:
Why this flag is needed:
The test_qnx macro uses helper rules (_get_test_and_data, _get_so_libs) that are marked as testonly and depend on cc_test targets. Without this flag, Bazel's trim_test_configuration automatically strips test configuration options from these dependencies, causing the same test target to be analyzed in multiple configurations:
- One configuration with full test options (for the main test suite)
- Another configuration with test options stripped (for the helper rules)
Both configurations try to produce identical output files (e.g., .pic.o object files), creating a conflict and failing the build.
By setting --experimental_retain_test_configuration_across_testonly, we preserve the test configuration across testonly rule boundaries, ensuring all analyses of the same test target use the same configuration. This is a targeted fix that only affects testonly targets and has no correctness impact.
Add a test target and wrap it with the corresponding QNX macro (see test/BUILD):
C++ (GTest)
load("@rules_cc//cc:defs.bzl", "cc_test")
load("@score_qnx_unit_tests//:defs.bzl", "cc_test_qnx")
cc_test(
name = "main_cpp",
srcs = ["main.cpp"],
deps = [
"@googletest//:gtest",
"@googletest//:gtest_main",
],
)
cc_test_qnx(
name = "main_cpp_qnx",
cc_test = ":main_cpp",
)Rust
load("@rules_rust//rust:defs.bzl", "rust_test")
load("@score_qnx_unit_tests//:defs.bzl", "rust_test_qnx")
rust_test(
name = "main_rust",
srcs = ["main_rust.rs"],
)
rust_test_qnx(
name = "main_rust_qnx",
rust_test = ":main_rust",
)Run the test:
# x86_64
bazel test --config=qnx-x86_64 //test:main_cpp_qnx
# aarch64
bazel test --config=qnx-aarch64 //test:main_cpp_qnxStream test output (useful for debugging):
bazel test --config=qnx-x86_64 //test:main_cpp_qnx --test_output=streamedAs an alternative to the cc_test_qnx / rust_test_qnx wrappers, you can run
existing cc_test or rust_test targets directly on QNX using the --run_under
configs. This packages and boots the test on-the-fly without requiring a wrapper
target:
# x86_64
bazel test --config=run-under-qnx-x86_64 //test:main_cpp
# aarch64
bazel test --config=run-under-qnx-aarch64 //test:main_cppThis mode runs all C++ and Rust test targets (no tag filter).
Launch an interactive QNX shell inside the microvm:
# x86_64
bazel run --config=qnx-x86_64 //test:main_cpp_qnx_shell
# aarch64
bazel run --config=qnx-aarch64 //test:main_cpp_qnx_shellThe test binary is available at /opt/tests/cc_test_qnx. Before running it,
execute the preparation script to set up the environment (runfiles, libraries,
gtest filters):
. /proc/boot/prepare_test.sh
/persistent/unit_tests/cc_test_qnxThe shell mode supports remote debugging via GDB. Specify a debug port:
bazel run -c dbg --config=qnx-x86_64 //test:main_cpp_qnx_shell -- --debug-port 38080Inside the QNX shell, run the prepare script before starting the debugger to set up runfiles, libraries, and environment variables:
. /proc/boot/prepare_test.shThen connect from the host. Note that the CWD must be set to /persistent/unit_tests
since the prepare script copies the binary and its runfiles there:
ntox86_64-gdb \
-ex "target qnx 127.0.0.1:38080" \
-ex "set nto-cwd /persistent/unit_tests" \
-ex "set nto-executable /persistent/unit_tests/cc_test_qnx" \
<path-to-debug-binary-in-bazel-bin>The cc_test_qnx and test_qnx macros support filtering out specific test cases (gtest only):
cc_test_qnx(
name = "main_cpp_qnx",
cc_test = ":main_cpp",
excluded_tests_filter = [
"FooTest.Test1", # Skip a single test
"BarTest.*", # Skip an entire suite
],
)| Variable | Default | Description |
|---|---|---|
DISABLE_KVM |
0 |
Set to 1 to disable KVM acceleration |
QEMU_CPU |
host |
QEMU CPU model (e.g. Cascadelake-Server-v5) |
FSDEV_PATH |
(auto) | Override the virtio-9p shared directory path |
For tests, pass via --test_env:
bazel test --config=qnx-x86_64 //test:main_cpp_qnx --test_env=DISABLE_KVM=1
bazel test --config=qnx-x86_64 //test:main_cpp_qnx --test_env=QEMU_CPU=Cascadelake-Server-v5For shell runs, set directly:
DISABLE_KVM=1 bazel run --config=qnx-x86_64 //test:main_cpp_qnx_shellThis project uses the Eclipse SCORE Bazel ecosystem:
| Module | Purpose |
|---|---|
score_bazel_cpp_toolchains |
QCC cross-compiler (GCC 12.2.0 for QNX SDP 8.0) |
score_toolchains_rust |
Ferrocene Rust toolchain for QNX |
score_rules_imagefs |
QNX IFS image generation (qnx_ifs rule) |
score_bazel_platforms |
Platform definitions and config_settings for QNX |
googletest |
Google Test framework |
rules_rust |
Rust build rules |