From 0fa783e0e6fa88acf8ebb1eb1fd8255b12d8404d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Mon, 31 Aug 2026 11:31:23 -0400 Subject: [PATCH] Added Renode regression tests for the NUCLEO-F401RE target The ARM target had no runtime test. Its CI job checked only that the ELF existed, so the clock configuration, the TIM2 HAL timebase, the console and the heap bound were all unexercised. This brings it to parity with the PolarFire target, which gates CI on a headless Renode run. Renode ships no NUCLEO-F401RE board description, so renode/nucleo_f401re.repl derives one from the generic STM32F4 CPU platform and corrects Flash to 512 KB and SRAM to 96 KB. The generic platform is sized for the larger F407/F429 parts, and both the linker script's heap reservation and the _sbrk() bound depend on those limits being right. main.c gains seven startup self-tests that run before tx_kernel_enter(), so a failure is reported even when the scheduler never starts. They cover the _sbrk() allocate, release, underflow and over-limit paths, the invariant that the heap reservation ends at or below the ThreadX byte pool, the 84 MHz SystemCoreClock, and that TIM2 still ticks after HAL_RCC_ClockConfig() re-enters HAL_InitTick(). Test 4 is the regression guard for the heap bound fixed in #50. Requesting 32 KB fits inside the 96 KB SRAM but far exceeds the heap reservation. Reinstating the old end-of-SRAM bound was verified to fail the suite: newlib's first malloc() then took roughly 4 KB and put the break at 0x20002170, inside the ThreadX byte pool, which also broke self-tests 1 and 3. With the bound correct, _sbrk() refuses the oversized request, newlib retries smaller, and malloc(64) succeeds using 72 bytes of the 512-byte reservation - so the reservation is adequate and no heap growth is needed. scripts/test_renode.py drives nucleo_f401re_ci.resc, which advances a fixed span of virtual time and quits on its own rather than depending on wall clock. Beyond the self-tests it asserts the boot banner, that the blink thread and the 1 Hz application timer have both run (covering the LED path and the timer service), and that the mutex, queue, event-flag and semaphore counters are all non-zero. The suite was confirmed to exit 1 on a reintroduced bug and 0 on the fixed tree. The Robot Framework suite covers the same ground for renode-test, and the new test-nucleo-renode CI job mirrors test-polarfire-renode. main.c also drops a hardcoded 0x20018000 in favour of BSP_RAM_END now that board_config.h is in scope, with the 4 KB main-stack margin named. --- .github/workflows/ci.yml | 33 ++++ .../NUCLEO_F401RE/README.md | 58 +++++- .../NUCLEO_F401RE/app/starter/main.c | 108 ++++++++++- .../NUCLEO_F401RE/renode/nucleo_f401re.repl | 22 +++ .../renode/nucleo_f401re_ci.resc | 35 ++++ .../renode/nucleo_f401re_demo.resc | 25 +++ .../renode/nucleo_f401re_demo.robot | 26 +++ .../NUCLEO_F401RE/scripts/test_renode.py | 172 ++++++++++++++++++ 8 files changed, 474 insertions(+), 5 deletions(-) create mode 100644 targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re.repl create mode 100644 targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_ci.resc create mode 100644 targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc create mode 100644 targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.robot create mode 100755 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a20de7..88e9bb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,3 +139,36 @@ jobs: name: nucleo-f401re-demo-elf path: targets/STMicroelectronics/NUCLEO_F401RE/build/app/nucleo_f401re.elf retention-days: 1 + + test-nucleo-renode: + name: Headless Renode Emulation & Assertion Test (NUCLEO-F401RE) + needs: build-arm-nucleo + runs-on: ubuntu-24.04 + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set Up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Download Built NUCLEO-F401RE ELF + uses: actions/download-artifact@v4 + with: + name: nucleo-f401re-demo-elf + path: targets/STMicroelectronics/NUCLEO_F401RE/build/app + + - name: Install Portable Renode Emulation Environment + run: | + wget -q https://builds.renode.io/renode-latest.linux-portable.tar.gz + mkdir -p $HOME/renode + tar -xzf renode-latest.linux-portable.tar.gz -C $HOME/renode --strip-components=1 + rm renode-latest.linux-portable.tar.gz + echo "$HOME/renode" >> $GITHUB_PATH + + - name: Run Deterministic Headless Renode Test + run: | + python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/README.md b/targets/STMicroelectronics/NUCLEO_F401RE/README.md index 828a66b..0c932dc 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/README.md +++ b/targets/STMicroelectronics/NUCLEO_F401RE/README.md @@ -50,6 +50,59 @@ The package includes build scripts under the `scripts/` directory for convenienc These scripts clean the build directory, run CMake configuration, and compile the target executable. +## Emulation & Regression Testing + +Renode ships no NUCLEO-F401RE board description, so `renode/nucleo_f401re.repl` +derives one from the generic STM32F4 CPU platform, correcting Flash to 512 KB +and SRAM to 96 KB. Those limits matter: the linker script's heap reservation and +the `_sbrk()` bound both depend on them. + +### Interactive Simulation + +```bash +renode targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc +``` + +USART2 is the demo console, shown in a terminal analyzer. + +### Automated Headless Test + +```bash +python3 targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py +``` + +Runs `nucleo_f401re_ci.resc`, which advances a fixed span of virtual time and +quits on its own, so the result does not depend on host speed. Exits non-zero +if any assertion is unmet. This gates CI as the `test-nucleo-renode` job. + +The Robot Framework suite in `renode/nucleo_f401re_demo.robot` covers the same +ground for use with `renode-test`. + +### What is asserted + +The demo runs seven startup self-tests before `tx_kernel_enter()` and prints a +`[SELF-TEST]` summary the harness asserts on: + +| # | Self-test | Guards against | +|---|-----------|----------------| +| 1 | `_sbrk()` allocation lands inside the heap reservation | heap escaping its linker reservation | +| 2 | `_sbrk()` releases back to the heap base | broken negative-increment path | +| 3 | `_sbrk()` underflow rejected with `EINVAL` | shrinking below the heap base | +| 4 | `_sbrk()` rejects a request that fits SRAM but not the heap | bounding the heap at the end of SRAM | +| 5 | Heap reservation ends at or below the ThreadX byte pool | linker script layout regression | +| 6 | `SystemCoreClock` is 84 MHz | a silently wrong PLL configuration | +| 7 | HAL timebase (TIM2) tick advancing | `HAL_InitTick()` re-entry leaving TIM2 stopped | + +Test 4 is the regression guard for the heap bound. Requesting 32 KB fits inside +the 96 KB SRAM but far exceeds the heap reservation, so bounding `_sbrk()` +against the end of SRAM rather than `_heap_limit` let it succeed and handed +`malloc()` memory owned by the ThreadX byte pool and the main stack. + +Beyond the self-tests, the harness asserts the boot banner reaches the console, +the blink thread and the 1 Hz application timer have both run (covering the LED +path and the timer service), and that the mutex, queue, event-flag and semaphore +counters are all non-zero. + ## Flashing the Application Connect the NUCLEO-F401RE board via the ST-LINK USB connector. @@ -136,14 +189,15 @@ The BSP overrides `HAL_InitTick()` to configure TIM2 as the HAL timebase and pro - **`lib/stm32cubef4/`**: HAL Driver wrapper and platform drivers - **`lib/threadx/`**: ThreadX user configuration file (`tx_user.h`) - **`cmake/`**: Cross-compilation module definitions -- **`scripts/`**: Utility build automation scripts +- **`scripts/`**: Utility build automation scripts and the headless Renode test runner +- **`renode/`**: Renode platform description, run scripts, and Robot Framework suite ## Validation Record ### Verification Environment - **Toolchain**: Arm GNU Toolchain 14.2.Rel1 (GCC 14.2.1), the version pinned by CI -- **Static ROM usage**: 20120 Bytes (3.84% of 512 KB Flash) +- **Static ROM usage**: 22068 Bytes (4.21% of 512 KB Flash), including the startup self-tests - **Static RAM usage**: 6000 Bytes (6.10% of 96 KB RAM) - **Dynamic Stack & Buffer allocation**: Stacks (8 x 1024 bytes) and Queue buffer (40 bytes) are dynamically allocated from the `TX_BYTE_POOL` (consuming 8312 bytes total, including pool headers). - **Board Hardware**: NUCLEO-F401RE diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c b/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c index 53b79b8..5493933 100644 --- a/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c +++ b/targets/STMicroelectronics/NUCLEO_F401RE/app/starter/main.c @@ -8,16 +8,25 @@ * SPDX-License-Identifier: MIT */ +#include +#include +#include #include #include "tx_api.h" #include "bsp/board.h" #include "bsp/led.h" #include "bsp/console.h" +#include "board_config.h" #include "cloud_config.h" +#include "stm32f4xx_hal.h" #define THREAD_STACK_SIZE 1024 +/* Bytes of SRAM held back above the ThreadX byte pool for the main stack, + * which serves every interrupt handler once the scheduler is running. */ +#define MAIN_STACK_MARGIN 4096 + typedef struct { CHAR *name; UINT state; @@ -348,9 +357,9 @@ void tx_application_define(void *first_unused_memory) { CHAR *stack_ptr; ULONG pool_size; - /* Calculate available RAM for the byte pool, leaving 4KB margin for system - * stack at the top (0x20018000) */ - pool_size = (0x20018000 - 4096) - (ULONG)first_unused_memory; + /* Calculate available RAM for the byte pool, leaving a 4 KB margin for the + * main stack at the top of SRAM. */ + pool_size = (BSP_RAM_END - MAIN_STACK_MARGIN) - (ULONG)first_unused_memory; /* Initialize the byte pool */ status = tx_byte_pool_create(&byte_pool, "system byte pool", @@ -540,9 +549,102 @@ void tx_application_define(void *first_unused_memory) { thread_registry[8] = &semaphore_thread; } +/* ------------------------------------------------------------------------- * + * Startup self-tests + * + * These run before tx_kernel_enter() so a failure is reported even when the + * scheduler never starts. scripts/test_renode.py asserts on the summary line. + * ------------------------------------------------------------------------- */ + +/* Defined by NUCLEO_F401RE.ld rather than by any translation unit. */ +extern char _end; +extern char _heap_limit; +extern char __RAM_segment_used_end__; + +extern void *_sbrk(ptrdiff_t incr); + +static unsigned selftest_failures = 0; + +static void selftest_report(int passed, const char *message) { + if (passed) { + printf("[+] PASS: %s\r\n", message); + } else { + selftest_failures++; + printf("[-] FAIL: %s\r\n", message); + } +} + +static void run_startup_self_tests(void) { + const uintptr_t heap_base = (uintptr_t)&_end; + const uintptr_t heap_limit = (uintptr_t)&_heap_limit; + const uintptr_t pool_base = (uintptr_t)&__RAM_segment_used_end__; + char msg[128]; + + printf("[SELF-TEST] Starting BSP & Runtime Verification...\r\n"); + + /* 1. A modest request lands inside the heap reservation. */ + void *p_ok = _sbrk(64); + selftest_report((p_ok != (void *)-1) && ((uintptr_t)p_ok >= heap_base) && + ((uintptr_t)p_ok < heap_limit), + "_sbrk() valid allocation inside the heap reservation"); + + /* 2. Releasing it returns the break to the heap base, leaving the heap + * exactly as the remaining tests found it. */ + selftest_report(_sbrk(-64) != (void *)-1, + "_sbrk() released 64 bytes back to the heap base"); + + /* 3. Shrinking below the heap base is rejected. */ + errno = 0; + void *p_under = _sbrk(-128); + selftest_report((p_under == (void *)-1) && (errno == EINVAL), + "_sbrk() underflow guard rejected with EINVAL"); + + /* 4. Regression guard for the heap bound. 32 KB fits inside the 96 KB SRAM + * but far exceeds the heap reservation, so bounding _sbrk() against the end + * of SRAM rather than _heap_limit let this succeed and handed malloc() + * memory owned by the ThreadX byte pool and the main stack. */ + errno = 0; + void *p_over = _sbrk((ptrdiff_t)0x8000); + selftest_report((p_over == (void *)-1) && (errno == ENOMEM), + "_sbrk() rejects a request that fits SRAM but not the heap"); + + /* 5. The heap reservation must end at or below the first byte ThreadX owns. */ + snprintf(msg, sizeof(msg), + "heap [0x%08lX,0x%08lX) ends at or below the ThreadX pool at 0x%08lX", + (unsigned long)heap_base, (unsigned long)heap_limit, + (unsigned long)pool_base); + selftest_report(heap_limit <= pool_base, msg); + + /* 6. SystemClock_Config() reached the documented 84 MHz. */ + snprintf(msg, sizeof(msg), "SystemCoreClock is %lu Hz (expected %lu Hz)", + (unsigned long)SystemCoreClock, (unsigned long)BSP_CPU_CLOCK_HZ); + selftest_report(SystemCoreClock == (uint32_t)BSP_CPU_CLOCK_HZ, msg); + + /* 7. The HAL timebase runs on TIM2 so ThreadX keeps SysTick. HAL_InitTick() + * is re-entered by HAL_RCC_ClockConfig() once the PLL is live, so confirm the + * timer is still ticking afterwards. The spin cap is a liveness bound, not a + * timing expectation: one TIM2 tick is ~84000 core cycles. */ + uint32_t tick_start = HAL_GetTick(); + uint32_t spins = 0; + while ((HAL_GetTick() == tick_start) && (spins < 5000000UL)) { + spins++; + } + selftest_report(HAL_GetTick() != tick_start, + "HAL timebase (TIM2) tick advancing"); + + if (selftest_failures == 0) { + printf("[SELF-TEST] All startup verification tests PASSED!\r\n\r\n"); + } else { + printf("[SELF-TEST] %u startup verification test(s) FAILED!\r\n\r\n", + selftest_failures); + } +} + int main(void) { bsp_board_init(); + run_startup_self_tests(); + /* Start the ThreadX kernel */ tx_kernel_enter(); diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re.repl b/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re.repl new file mode 100644 index 0000000..b6c15ae --- /dev/null +++ b/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re.repl @@ -0,0 +1,22 @@ +// Copyright (c) 2026 Eclipse ThreadX contributors +// +// This program and the accompanying materials are made available +// under the terms of the MIT license which is available at +// https://opensource.org/license/mit. +// +// SPDX-License-Identifier: MIT +// +// ST NUCLEO-F401RE (STM32F401RET6, Cortex-M4F). +// +// Renode ships no NUCLEO-F401RE board description, so this derives one from the +// generic STM32F4 CPU platform. That platform is sized for the larger F407/F429 +// parts; the F401RE has 512 KB Flash and 96 KB SRAM, and the demo's linker +// script and _sbrk() bound both depend on those limits being accurate. + +using "platforms/cpus/stm32f4.repl" + +sram: + size: 0x18000 + +flash: + size: 0x80000 diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_ci.resc b/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_ci.resc new file mode 100644 index 0000000..4294976 --- /dev/null +++ b/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_ci.resc @@ -0,0 +1,35 @@ +:name: NUCLEO-F401RE - ThreadX Demo (headless CI run) +:description: Deterministic virtual-time run driven by scripts/test_renode.py. + +# Standalone counterpart to nucleo_f401re_demo.resc rather than an include of +# it. Renode resolves $ORIGIN only in variable assignment, so an included script +# cannot be located relative to the file including it, and the two run +# differently anyway: the demo script free-runs for interactive use, while this +# one advances a fixed span of virtual time so the run is reproducible and +# terminates on its own instead of depending on wall clock. + +Clear + +using sysbus +mach create "NUCLEO_F401RE" + +$platform?=$ORIGIN/nucleo_f401re.repl +machine LoadPlatformDescription $platform + +# Under --plain --disable-gui this logs USART2 traffic to stdout, which is what +# the assertions in test_renode.py read. +showAnalyzer usart2 + +$bin?=$ORIGIN/../build/app/nucleo_f401re.elf + +macro reset +""" + sysbus LoadELF $bin +""" +runMacro $reset + +# Long enough for the startup self-tests, the 1 Hz application timer, and at +# least two reporter-thread status blocks with non-zero RTOS counters. +emulation RunFor "4" + +quit diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc b/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc new file mode 100644 index 0000000..53497f0 --- /dev/null +++ b/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.resc @@ -0,0 +1,25 @@ +:name: NUCLEO-F401RE - ThreadX Device Monitor Demo +:description: Eclipse ThreadX RTOS on ST NUCLEO-F401RE (Renode, ARM Cortex-M4) + +# Clear previous emulation state +Clear + +using sysbus +mach create "NUCLEO_F401RE" + +$platform?=$ORIGIN/nucleo_f401re.repl +machine LoadPlatformDescription $platform + +# USART2 is the ST-LINK virtual COM port on this board and the demo console. +showAnalyzer usart2 + +# Portable relative path using Renode's built-in $ORIGIN variable +$bin?=$ORIGIN/../build/app/nucleo_f401re.elf + +macro reset +""" + sysbus LoadELF $bin +""" +runMacro $reset + +start diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.robot b/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.robot new file mode 100644 index 0000000..20f94ee --- /dev/null +++ b/targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_demo.robot @@ -0,0 +1,26 @@ +*** Settings *** +Suite Setup Setup +Suite Teardown Teardown +Test Setup Reset Emulation +Test Teardown Test Teardown +Resource ${RENODEKEYWORDS} + +*** Test Cases *** +Should Pass Startup Self-Tests + Execute Command include @${CURDIR}/nucleo_f401re_demo.resc + Create Terminal Tester sysbus.usart2 + + Wait For Line On Uart NUCLEO-F401RE Device Monitor Demo timeout=15 + Wait For Line On Uart [SELF-TEST] Starting BSP & Runtime Verification... timeout=15 + Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15 + +Should Run ThreadX Scheduler And Exercise RTOS Primitives + Execute Command include @${CURDIR}/nucleo_f401re_demo.resc + Create Terminal Tester sysbus.usart2 + + Wait For Line On Uart [SELF-TEST] All startup verification tests PASSED! timeout=15 + Wait For Line On Uart System Status: timeout=15 + # The blink thread drives bsp_led_toggle() on PA5 and the application timer + # drives the wake counter, so a non-zero pair covers both BSP paths. + Wait For Line On Uart Runs: Monitor: (\\d+) .* Blink: [1-9]\\d* timeout=20 treatAsRegex=true + Wait For Line On Uart Mutex Locks: [1-9]\\d*.*Queue Msgs: [1-9]\\d* timeout=20 treatAsRegex=true diff --git a/targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py b/targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py new file mode 100755 index 0000000..e37a8a7 --- /dev/null +++ b/targets/STMicroelectronics/NUCLEO_F401RE/scripts/test_renode.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2026 Eclipse ThreadX contributors +# +# This program and the accompanying materials are made available +# under the terms of the MIT license which is available at +# https://opensource.org/licenses/MIT. +# +# SPDX-License-Identifier: MIT +# + +""" +Headless Renode Verification Test for the NUCLEO-F401RE ThreadX Demo. + +Runs targets/STMicroelectronics/NUCLEO_F401RE/renode/nucleo_f401re_ci.resc and +asserts on the USART2 console output. The run advances a fixed span of virtual +time and quits on its own, so the result does not depend on host speed. +""" + +import os +import re +import shutil +import subprocess +import sys +import threading +import queue +import time + +RESC_NAME = "nucleo_f401re_ci.resc" + +# The reporter thread prints these once the RTOS primitives have been exercised. +RUNS_RE = re.compile(r"Runs: Monitor: (\d+) \| Reporter: (\d+) \| Blink: (\d+) \| Timer Wakes: (\d+)") +RTOS_RE = re.compile( + r"Mutex Locks: (\d+)/(\d+) \| Queue Msgs: (\d+) \| Event Wakes: (\d+) \| Sema Wakes: (\d+)" +) + + +def find_renode(): + renode_bin = shutil.which("renode") + if renode_bin: + return renode_bin + + win_paths = [ + r"C:\Program Files\Renode\renode.exe", + os.path.expanduser(r"~\AppData\Local\Programs\Renode\renode.exe"), + ] + for path in win_paths: + if os.path.isfile(path): + return path + + return "renode" + + +def reader_thread_fn(pipe, q): + try: + for line in iter(pipe.readline, ""): + q.put(line) + except Exception: + pass + finally: + pipe.close() + + +def run_test(test_timeout_mode=False): + renode = find_renode() + script_dir = os.path.dirname(os.path.abspath(__file__)) + target_dir = os.path.dirname(script_dir) + resc_path = os.path.join(target_dir, "renode", RESC_NAME).replace("\\", "/") + + if test_timeout_mode: + print("[*] Running intentional timeout test mode (2.0s deadline)...") + timeout_seconds = 2.0 + else: + print(f"[*] Starting headless Renode test using: {renode}") + print(f"[*] Loading script: {resc_path}") + timeout_seconds = 600.0 + + cmd = [renode, "--plain", "--disable-gui", "--port", "-1", + "-e", f"include @{resc_path}"] + + proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + + output_q = queue.Queue() + reader_t = threading.Thread(target=reader_thread_fn, args=(proc.stdout, output_q), daemon=True) + reader_t.start() + + found_banner = False + found_selftests = False + found_selftest_failure = False + found_led_and_timer = False + found_rtos_primitives = False + start_time = time.time() + + try: + while time.time() - start_time < timeout_seconds: + try: + line = output_q.get(timeout=0.1) + print(line, end="") + if test_timeout_mode: + continue + + if "NUCLEO-F401RE Device Monitor Demo" in line: + found_banner = True + if "[-] FAIL:" in line or "startup verification test(s) FAILED" in line: + found_selftest_failure = True + if "[SELF-TEST] All startup verification tests PASSED!" in line: + found_selftests = True + + # The blink thread is the only caller of bsp_led_toggle(), and + # timer wakes come from the 1 Hz ThreadX application timer, so a + # non-zero pair covers the LED path and the timer service. + m = RUNS_RE.search(line) + if m and int(m.group(3)) > 0 and int(m.group(4)) > 0: + found_led_and_timer = True + + m = RTOS_RE.search(line) + if m and all(int(g) > 0 for g in m.groups()): + found_rtos_primitives = True + + # A failed self-test is recorded but does not stop the run, so + # the remaining assertions are still reported rather than hidden. + if (found_banner and found_selftests and not found_selftest_failure + and found_led_and_timer and found_rtos_primitives): + print("\n[+] SUCCESS: boot banner, startup self-tests, LED and " + "timer activity, and all RTOS primitives verified!") + break + except queue.Empty: + if proc.poll() is not None: + break + finally: + try: + proc.terminate() + proc.wait(timeout=5) + except Exception: + try: + proc.kill() + except Exception: + pass + + if test_timeout_mode: + elapsed = time.time() - start_time + print(f"\n[+] SUCCESS: Intentional timeout triggered after {elapsed:.2f}s " + f"and terminated child process cleanly.") + sys.exit(0) + + checks = { + "boot banner reached the console": found_banner, + "startup self-tests passed": found_selftests and not found_selftest_failure, + "LED blink thread and application timer ran": found_led_and_timer, + "mutex, queue, event flag and semaphore all exercised": found_rtos_primitives, + } + failed = [name for name, ok in checks.items() if not ok] + + if not failed: + print("[+] Renode headless test PASSED.") + sys.exit(0) + + print("\n[-] FAILED. Unmet assertions:") + for name in failed: + print(" - %s" % name) + sys.exit(1) + + +if __name__ == "__main__": + run_test(test_timeout_mode="--test-timeout" in sys.argv)