Establish a standardized, native-feeling Starlark macro (ztest) that enables Zephyr's ZTest-based integration and unit test applications to be executed hermetically and cacheably using standard Bazel test workflows: bazel test //path/to:test.
Zephyr RTOS unit and integration tests rely extensively on the ZTest framework. These tests are fundamentally structured as standalone Zephyr applications (zephyr_app targets) that boot the Zephyr kernel, execute a suite of tests inside the simulated or physical hardware, and print test results to the serial console.
In our Bazel configuration, target firmware binaries are built as executable rules (using the zephyr_app transition rule wrapper). When building for the host simulator platform (@zephyr//boards/native/native_sim:native), the resulting binary can be executed directly in the host terminal (using bazel run), booting the simulator and running the tests.
However, this creates severe limitations for automated workflows:
bazel test Failure: Executing bazel test on a target binary fails with ERROR: No test targets were found, yet testing was requested. Standard test rules (cc_test) cannot link against the Zephyr kernel or target BSP files in our overlay.bazel run bypasses Bazel's sandboxing and caching features. If a test succeeds, its results cannot be cached by Bazel, forcing complete re-execution."no_test": true inside workflows.json) and only verify that they compile.ztest Starlark MacroWe propose implementing a custom Bazel test macro called ztest that seamlessly bridges the gap between Zephyr target applications and host-side Bazel test runners.
Instead of demanding developers to write custom shell runners or maintain complex wrapper configurations, a developer can simply declare a ztest target in their BUILD.bazel.
load("@zephyr//:cc_test.bzl", "ztest") ztest( name = "my_integration_test", deps = [ ":test_sources", ], )
The ztest macro automatically sets up the following build graph:
graph TD UserTest[ztest: my_test] -->|Generates| Bin[zephyr_app: my_test_bin] UserTest -->|Generates| Copy[copy_file: my_test_runner_copy] UserTest -->|Generates| Test[py_test: my_test] Copy -->|Copies| Script["Script:<br>my_test_runner.py"] Test -->|Runs| Script Test -->|Data Dependency| Bin Script -->|1. Runs| Bin Script -->|2. Parses Console| Match{"Matches Success Signature?"} Match -->|Yes| Success[Exit 0: Test Passed] Match -->|No| Fail[Exit 1: Test Failed]
zephyr_app): The macro compiles a transition-ruled zephyr_app firmware target (name + "_bin") containing the ZTest binary.copy_file rule (name + "_runner_copy") from bazel_skylib to copy the shared static Python runner script (ztest_runner.py) to the local package, working around Bazel's package constraints on py_test's main attribute.py_test): The macro wraps the copied runner script in a standard py_test target. This automatically binds the execution to the hermetic Python toolchain managed by rules_python, eliminating host-environment dependencies.native_sim simulator (passing -uart_stdinout and guest-side/host-side timeouts to prevent hangs).PROJECT EXECUTION SUCCESSFUL), integrates exit code validation, and propagates the exit code.target_compatible_with to only run on simulator platforms (@zephyr//boards/native:sim), avoiding execution on physical platforms.Zephyr RTOS targets fundamentally communicate through a physical or simulated UART console device. This maps to a single, shared output channel:
stdout.stderr), the Python runner redirects and captures both pipelines: subprocess.run(..., stdout=subprocess.PIPE, stderr=subprocess.STDOUT).0, the runner validates the presence of the success_signature.success_signature attribute (defaulting to "PROJECT EXECUTION SUCCESSFUL").XML_OUTPUT_FILE environment variable. The Python runner captures this path and automatically generates a standard JUnit XML report capturing test suites, test execution, and raw logs inside this file, allowing deep integration with CI/CD test dashboards.#!/usr/bin/env python3). Instead, it is packaged using the hermetic py_test toolchain defined by rules_python to guarantee identical script execution across all environments.stdout): The merged stream (Zephyr kernel logs + compiler warnings + ZTest results) is streamed to stdout so Bazel preserves it in the persistent test.log output.stderr): Runner-level failures (e.g., timeout limits reached, failure to locate the "PROJECT EXECUTION SUCCESSFUL" signature, process crashes) are written directly to the runner‘s stderr. This ensures diagnostic summaries print cleanly onto the developer’s terminal when a test fails.We define the public ztest macro and the helper _ztest_runner_gen rule inside bazel_overlay/cc_test.bzl.
The implementation details of the ztest macro, platform selections, and the underlying py_test setup are in bazel_overlay/cc_test.bzl.
The ztest macro strictly requires a host-executable binary because it executes the compiled Zephyr application directly on the host machine using the Zephyr native_sim simulator.
[!WARNING] If this test target were allowed to build on a non-host architecture (e.g., ARM Cortex-M) without emulator support, executing the test would fail at runtime with an
Exec format erroron the host.Consequently, the macro unconditionally restricts compatibility to simulator platforms (like
@zephyr//boards/native:sim). If hardware-in-the- loop (HIL) testing or QEMU emulator execution is required in the future, a separate runner and macro implementation must be introduced.
To ensure the correctness and long-term maintainability of the ztest macro, rule, and generated runner script, we will implement a multi-tiered testing strategy.
Since analysis-phase unit tests require heavy mocking of Zephyr's underlying configuration system (Kconfig, DTS, build transitions), we rely on actual compiling end-to-end targets inside our codebase to verify the build graph and runner execution.
We will add mock applications in examples/ to verify the system:
//examples/ztest_mock_pass): A minimal ZTest app that succeeds. Running bazel test //examples/ztest_mock_pass must pass and be cacheable by Bazel.//examples/ztest_mock_fail): A minimal ZTest app that deliberately fails (e.g. zassert_true(false)). Running bazel test //examples/ztest_mock_fail must fail cleanly and return a non-zero exit code to the terminal.Since the Python runner script is generated inline inside Starlark, testing it directly can be difficult. To address this, we will extract the runner's core execution and parsing logic into a shared, unit-testable Python script inside scripts/build/ztest_runner_core.py, while the Starlark rule writes a small “bootstrap” file that imports this logic.
scripts/build/ztest_runner_core_test.py (New)unittest library, executed via a py_test target.RUNFILES_DIR, TEST_WORKSPACE) to verify that the runner correctly resolves the absolute path of the simulator binary under different sandboxing conditions.subprocess.run to return returncode = 0 and stdout containing PROJECT EXECUTION SUCCESSFUL. Assert that the runner exits with code 0.subprocess.run to return a non-zero exit code (e.g., 1) but stdout containing PROJECT EXECUTION SUCCESSFUL. Assert that the runner correctly exits with code 1.subprocess.run to return returncode = 0 but empty stdout. Assert that the runner exits with code 1 and prints a descriptive error.subprocess.run to throw subprocess.TimeoutExpired. Assert that the runner terminates safely, prints the accumulated logs, and exits with code 1.Maintain concrete .sh scripts in individual test directories and execute them using standard sh_test rules.
To address the limitations of the current custom simulator runner, we plan to explore a deeper integration with Zephyr's Twister framework and support for broader execution environments:
While the current runner relies on simple string matching (PROJECT EXECUTION SUCCESSFUL), future versions could leverage Twister's parsing libraries to:
To support running tests on architectures other than the host simulator (e.g., ARM Cortex-M targets):
ztest macro to support launching emulators like QEMU or FVP using Bazel-runnable toolchain wrappers.zephyr_app test binaries on physical hardware boards connected to the host or runner environments.