From 8706cd6135a27a6af1349cd14973371e19547589 Mon Sep 17 00:00:00 2001 From: Guillaume Souchere Date: Tue, 31 Mar 2026 12:54:47 +0200 Subject: [PATCH] feat(esp_system): add linux test for system init function regisration --- .../check_system_init_priorities.py | 68 ++++++---- components/esp_system/startup.c | 8 ++ .../test_apps/sys_init_fn/CMakeLists.txt | 9 ++ .../test_apps/sys_init_fn/main/CMakeLists.txt | 4 + .../sys_init_fn/main/test_init_fn_defs.c | 75 +++++++++++ .../sys_init_fn/main/test_init_fn_defs.h | 28 ++++ .../sys_init_fn/main/test_sys_init_fn.c | 124 ++++++++++++++++++ .../sys_init_fn/pytest_sys_init_fn.py | 18 +++ .../test_apps/sys_init_fn/sdkconfig.defaults | 1 + .../FreeRTOS-Kernel/portable/linux/port_idf.c | 2 +- tools/mocks/freertos/CMakeLists.txt | 4 + tools/mocks/freertos/Kconfig | 1 + 12 files changed, 319 insertions(+), 23 deletions(-) create mode 100644 components/esp_system/test_apps/sys_init_fn/CMakeLists.txt create mode 100644 components/esp_system/test_apps/sys_init_fn/main/CMakeLists.txt create mode 100644 components/esp_system/test_apps/sys_init_fn/main/test_init_fn_defs.c create mode 100644 components/esp_system/test_apps/sys_init_fn/main/test_init_fn_defs.h create mode 100644 components/esp_system/test_apps/sys_init_fn/main/test_sys_init_fn.c create mode 100644 components/esp_system/test_apps/sys_init_fn/pytest_sys_init_fn.py create mode 100644 components/esp_system/test_apps/sys_init_fn/sdkconfig.defaults diff --git a/components/esp_system/check_system_init_priorities.py b/components/esp_system/check_system_init_priorities.py index 29f420cf81..555f64d7e1 100644 --- a/components/esp_system/check_system_init_priorities.py +++ b/components/esp_system/check_system_init_priorities.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 # # This file is used to check the order of execution of ESP_SYSTEM_INIT_FN functions. @@ -13,12 +13,14 @@ import itertools import os import re import sys -import typing -ESP_SYSTEM_INIT_FN_STR = r'ESP_SYSTEM_INIT_FN' -ESP_SYSTEM_INIT_FN_REGEX_SIMPLE = re.compile(r'ESP_SYSTEM_INIT_FN') -ESP_SYSTEM_INIT_FN_REGEX = re.compile(r'ESP_SYSTEM_INIT_FN\(([a-zA-Z0-9_]+)\s*,\s*([a-zA-Z\ _0-9\(\)|]+)\s*,\s*([a-zA-Z\ _0-9\(\)|]+)\s*,\s*([0-9]+)\)') +COMMENT_REGEX = re.compile(r'//.*?$|/\*.*?\*/', re.DOTALL | re.MULTILINE) +ESP_SYSTEM_INIT_FN_REGEX_SIMPLE = re.compile(r'\bESP_SYSTEM_INIT_FN\s*\(') +ESP_SYSTEM_INIT_FN_REGEX = re.compile( + r'ESP_SYSTEM_INIT_FN\(([a-zA-Z0-9_]+)\s*,\s*([a-zA-Z\ _0-9\(\)|]+)\s*,\s*([a-zA-Z\ _0-9\(\)|]+)\s*,\s*([0-9]+)\)' +) STARTUP_ENTRIES_FILE = 'components/esp_system/system_init_fn.txt' +EXCLUDED_SOURCE_DIRS = {'test_apps', 'host_test', 'host_tests'} class StartupEntry: @@ -33,6 +35,15 @@ class StartupEntry: return f'{self.stage}: {self.priority:3d}: {self.func} in {self.filename} on {self.affinity}' +def should_skip_source_file(filename: str, idf_path: str) -> bool: + relpath_parts = os.path.relpath(filename, idf_path).split(os.sep) + return any(part in EXCLUDED_SOURCE_DIRS for part in relpath_parts) + + +def strip_comments(contents: str) -> str: + return COMMENT_REGEX.sub('', contents) + + def main() -> None: try: idf_path = os.environ['IDF_PATH'] @@ -40,7 +51,7 @@ def main() -> None: raise SystemExit('IDF_PATH must be set before running this script') has_errors = False - startup_entries = [] # type: typing.List[StartupEntry] + startup_entries: list[StartupEntry] = [] # # 1. Iterate over all .c and .cpp source files and find ESP_SYSTEM_INIT_FN definitions @@ -50,24 +61,32 @@ def main() -> None: glob_iter = glob.glob(os.path.join(idf_path, 'components', '**', f'*.{extension}'), recursive=True) source_files_iters.append(glob_iter) for filename in itertools.chain(*source_files_iters): - with open(filename, 'r', encoding='utf-8') as f_obj: - file_contents = f_obj.read() - if ESP_SYSTEM_INIT_FN_STR not in file_contents: + if should_skip_source_file(filename, idf_path): continue - count_expected = len(ESP_SYSTEM_INIT_FN_REGEX_SIMPLE.findall(file_contents)) - found = ESP_SYSTEM_INIT_FN_REGEX.findall(file_contents) + + relpath = os.path.relpath(filename, idf_path) + with open(filename, encoding='utf-8') as f_obj: + file_contents = f_obj.read() + + file_contents_no_comments = strip_comments(file_contents) + if not ESP_SYSTEM_INIT_FN_REGEX_SIMPLE.search(file_contents_no_comments): + continue + + count_expected = len(ESP_SYSTEM_INIT_FN_REGEX_SIMPLE.findall(file_contents_no_comments)) + found = ESP_SYSTEM_INIT_FN_REGEX.findall(file_contents_no_comments) if len(found) != count_expected: - print((f'error: In {filename}, found ESP_SYSTEM_INIT_FN {count_expected} time(s), ' - f'but regular expression matched {len(found)} time(s)'), file=sys.stderr) + print( + ( + f'error: In {filename}, found ESP_SYSTEM_INIT_FN {count_expected} time(s), ' + f'but regular expression matched {len(found)} time(s)' + ), + file=sys.stderr, + ) has_errors = True for match in found: entry = StartupEntry( - filename=os.path.relpath(filename, idf_path), - func=match[0], - stage=match[1], - affinity=match[2], - priority=int(match[3]) + filename=relpath, func=match[0], stage=match[1], affinity=match[2], priority=int(match[3]) ) startup_entries.append(entry) @@ -77,7 +96,7 @@ def main() -> None: # to have a stable sorting order in case when the same startup function is defined in multiple files, # for example for different targets. # - def sort_key(entry: StartupEntry) -> typing.Tuple[str, int, str]: + def sort_key(entry: StartupEntry) -> tuple[str, int, str]: # luckily 'core' and 'secondary' are in alphabetical order, so we can return the string return (entry.stage, entry.priority, entry.filename) @@ -88,7 +107,7 @@ def main() -> None: # 3. Load startup entries list from STARTUP_ENTRIES_FILE, removing comments and empty lines # startup_entries_expected_lines = [] - with open(os.path.join(idf_path, STARTUP_ENTRIES_FILE), 'r', encoding='utf-8') as startup_entries_expected_file: + with open(os.path.join(idf_path, STARTUP_ENTRIES_FILE), encoding='utf-8') as startup_entries_expected_file: for line in startup_entries_expected_file: if line.startswith('#') or len(line.strip()) == 0: continue @@ -99,8 +118,13 @@ def main() -> None: # diff_lines = list(difflib.unified_diff(startup_entries_expected_lines, startup_entries_lines, lineterm='')) if len(diff_lines) > 0: - print(('error: startup order doesn\'t match the reference file. ' - f'please update {STARTUP_ENTRIES_FILE} to match the actual startup order:'), file=sys.stderr) + print( + ( + "error: startup order doesn't match the reference file. " + f'please update {STARTUP_ENTRIES_FILE} to match the actual startup order:' + ), + file=sys.stderr, + ) for line in diff_lines: print(f'{line}', file=sys.stderr) has_errors = True diff --git a/components/esp_system/startup.c b/components/esp_system/startup.c index 91ba435c43..5d68d49c01 100644 --- a/components/esp_system/startup.c +++ b/components/esp_system/startup.c @@ -229,3 +229,11 @@ static void start_cpu0_default(void) ESP_INFINITE_LOOP(); } + +#if CONFIG_IDF_TARGET_LINUX && !defined(ESP_SYSTEM_LINUX_NO_MAIN) +__attribute__((weak)) int main(int argc, char **argv) +{ + start_cpu0(); + return 0; +} +#endif // CONFIG_IDF_TARGET_LINUX diff --git a/components/esp_system/test_apps/sys_init_fn/CMakeLists.txt b/components/esp_system/test_apps/sys_init_fn/CMakeLists.txt new file mode 100644 index 0000000000..841ddfdd79 --- /dev/null +++ b/components/esp_system/test_apps/sys_init_fn/CMakeLists.txt @@ -0,0 +1,9 @@ +# For more information about build system see +# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) +project(test_sys_init_fn) diff --git a/components/esp_system/test_apps/sys_init_fn/main/CMakeLists.txt b/components/esp_system/test_apps/sys_init_fn/main/CMakeLists.txt new file mode 100644 index 0000000000..de763979b0 --- /dev/null +++ b/components/esp_system/test_apps/sys_init_fn/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register(SRCS "test_sys_init_fn.c" + "test_init_fn_defs.c" + INCLUDE_DIRS "." + PRIV_REQUIRES unity esp_system) diff --git a/components/esp_system/test_apps/sys_init_fn/main/test_init_fn_defs.c b/components/esp_system/test_apps/sys_init_fn/main/test_init_fn_defs.c new file mode 100644 index 0000000000..9343571584 --- /dev/null +++ b/components/esp_system/test_apps/sys_init_fn/main/test_init_fn_defs.c @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * This file defines system init functions whose execution is verified + * by the test cases in test_sys_init_fn.c. + * + * The functions are placed into the esp_sys_init_fn linker section via the + * system init macro. On Linux this uses ELF section sorting; on macOS + * the same section is resolved at runtime via getsectiondata(). + * + * Important: this file must be compiled into the same binary as the test + * runner so the linker/loader sees the section entries. + */ + +#include "esp_private/startup_internal.h" +#include "test_init_fn_defs.h" + +/* ---- Global state read back by the test cases ---- */ + +int trace_log[INIT_FN_TRACE_MAX]; +int trace_count = 0; + +bool core_prio_200_executed = false; +bool core_prio_250_executed = false; +bool secondary_prio_200_executed = false; +bool secondary_prio_250_executed = false; + +/* ---- Helper ---- */ +static void trace_append(int tag) +{ + if (trace_count < INIT_FN_TRACE_MAX) { + trace_log[trace_count++] = tag; + } +} + +/* ---- CORE-stage init functions (executed during do_core_init) ---- */ + +/* + * Two CORE-stage functions with different priorities. + * Priority 200 must execute before priority 250. + * We use tag values equal to the priority for easy identification. + */ +ESP_SYSTEM_INIT_FN(test_core_prio_200, CORE, BIT(0), 200) +{ + core_prio_200_executed = true; + trace_append(200); + return ESP_OK; +} + +ESP_SYSTEM_INIT_FN(test_core_prio_250, CORE, BIT(0), 250) +{ + core_prio_250_executed = true; + trace_append(250); + return ESP_OK; +} + +/* ---- SECONDARY-stage init functions (executed during do_secondary_init) ---- */ + +ESP_SYSTEM_INIT_FN(test_secondary_prio_200, SECONDARY, BIT(0), 200) +{ + secondary_prio_200_executed = true; + trace_append(1200); /* offset by 1000 so we can distinguish stage in trace */ + return ESP_OK; +} + +ESP_SYSTEM_INIT_FN(test_secondary_prio_250, SECONDARY, BIT(0), 250) +{ + secondary_prio_250_executed = true; + trace_append(1250); + return ESP_OK; +} diff --git a/components/esp_system/test_apps/sys_init_fn/main/test_init_fn_defs.h b/components/esp_system/test_apps/sys_init_fn/main/test_init_fn_defs.h new file mode 100644 index 0000000000..94374297aa --- /dev/null +++ b/components/esp_system/test_apps/sys_init_fn/main/test_init_fn_defs.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +/** + * Maximum number of init function invocations we track for ordering tests. + */ +#define INIT_FN_TRACE_MAX 16 + +/** + * Global trace log filled by init functions to record their execution order. + * Each init function appends its own tag (an arbitrary integer) to + * trace_log[trace_count] and increments trace_count. + */ +extern int trace_log[INIT_FN_TRACE_MAX]; +extern int trace_count; + +/* Flags set by individual init functions so tests can verify they executed */ +extern bool core_prio_200_executed; +extern bool core_prio_250_executed; +extern bool secondary_prio_200_executed; +extern bool secondary_prio_250_executed; diff --git a/components/esp_system/test_apps/sys_init_fn/main/test_sys_init_fn.c b/components/esp_system/test_apps/sys_init_fn/main/test_sys_init_fn.c new file mode 100644 index 0000000000..1720b5cd41 --- /dev/null +++ b/components/esp_system/test_apps/sys_init_fn/main/test_sys_init_fn.c @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * Test cases for the ESP_SYSTEM_INIT_FN mechanism. + * + * The init functions are defined in test_init_fn_defs.c and are executed + * automatically during startup (before app_main). These tests inspect the + * side-effects left by those functions to verify: + * + * 1. Init functions actually executed. + * 2. CORE-stage functions executed before SECONDARY-stage functions. + * 3. Within the same stage, lower priority values execute first. + * 4. All expected functions ran (no silent drops). + */ + +#include +#include "unity.h" +#include "unity_test_runner.h" +#include "test_init_fn_defs.h" + +/* ---------- Test cases ---------- */ + +TEST_CASE("CORE init functions executed", "[sys_init_fn]") +{ + TEST_ASSERT_TRUE_MESSAGE(core_prio_200_executed, + "CORE priority-200 init function did not execute"); + TEST_ASSERT_TRUE_MESSAGE(core_prio_250_executed, + "CORE priority-250 init function did not execute"); +} + +TEST_CASE("SECONDARY init functions executed", "[sys_init_fn]") +{ + TEST_ASSERT_TRUE_MESSAGE(secondary_prio_200_executed, + "SECONDARY priority-200 init function did not execute"); + TEST_ASSERT_TRUE_MESSAGE(secondary_prio_250_executed, + "SECONDARY priority-250 init function did not execute"); +} + +TEST_CASE("all four init functions traced", "[sys_init_fn]") +{ + /* We registered 4 init functions total (2 CORE + 2 SECONDARY) */ + TEST_ASSERT_GREATER_OR_EQUAL_INT_MESSAGE(4, trace_count, + "expected at least 4 init function traces"); +} + +TEST_CASE("CORE stage runs before SECONDARY stage", "[sys_init_fn]") +{ + /* + * In the trace log, CORE entries have tags < 1000, + * SECONDARY entries have tags >= 1000. + * All CORE entries must appear before any SECONDARY entry. + */ + int first_secondary = -1; + int last_core = -1; + + for (int i = 0; i < trace_count; i++) { + if (trace_log[i] < 1000) { + last_core = i; + } else if (first_secondary < 0) { + first_secondary = i; + } + } + + TEST_ASSERT_TRUE_MESSAGE(last_core >= 0, "no CORE trace entries found"); + TEST_ASSERT_TRUE_MESSAGE(first_secondary >= 0, "no SECONDARY trace entries found"); + TEST_ASSERT_LESS_THAN_INT_MESSAGE(first_secondary, last_core, + "a CORE init function ran after a SECONDARY one"); +} + +TEST_CASE("priority ordering within CORE stage", "[sys_init_fn]") +{ + /* + * Within the CORE stage, priority 200 (tag 200) must appear + * before priority 250 (tag 250) in the trace log. + */ + int pos_200 = -1; + int pos_250 = -1; + + for (int i = 0; i < trace_count; i++) { + if (trace_log[i] == 200 && pos_200 < 0) { + pos_200 = i; + } + if (trace_log[i] == 250 && pos_250 < 0) { + pos_250 = i; + } + } + + TEST_ASSERT_TRUE_MESSAGE(pos_200 >= 0, "CORE prio-200 not in trace"); + TEST_ASSERT_TRUE_MESSAGE(pos_250 >= 0, "CORE prio-250 not in trace"); + TEST_ASSERT_LESS_THAN_INT_MESSAGE(pos_250, pos_200, + "CORE prio-200 did not run before prio-250"); +} + +TEST_CASE("priority ordering within SECONDARY stage", "[sys_init_fn]") +{ + int pos_1200 = -1; + int pos_1250 = -1; + + for (int i = 0; i < trace_count; i++) { + if (trace_log[i] == 1200 && pos_1200 < 0) { + pos_1200 = i; + } + if (trace_log[i] == 1250 && pos_1250 < 0) { + pos_1250 = i; + } + } + + TEST_ASSERT_TRUE_MESSAGE(pos_1200 >= 0, "SECONDARY prio-200 not in trace"); + TEST_ASSERT_TRUE_MESSAGE(pos_1250 >= 0, "SECONDARY prio-250 not in trace"); + TEST_ASSERT_LESS_THAN_INT_MESSAGE(pos_1250, pos_1200, + "SECONDARY prio-200 did not run before prio-250"); +} + +/* ---------- Entry point ---------- */ + +void app_main(void) +{ + printf("Running sys_init_fn host test app\n"); + unity_run_menu(); +} diff --git a/components/esp_system/test_apps/sys_init_fn/pytest_sys_init_fn.py b/components/esp_system/test_apps/sys_init_fn/pytest_sys_init_fn.py new file mode 100644 index 0000000000..e26356da40 --- /dev/null +++ b/components/esp_system/test_apps/sys_init_fn/pytest_sys_init_fn.py @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import pytest +from pytest_embedded_idf.dut import IdfDut +from pytest_embedded_idf.utils import idf_parametrize + + +@pytest.mark.host_test +@idf_parametrize('target', ['linux'], indirect=['target']) +def test_sys_init_fn_linux(dut: IdfDut) -> None: + dut.run_all_single_board_cases(timeout=60) + + +@pytest.mark.host_test +@pytest.mark.macos +@idf_parametrize('target', ['linux'], indirect=['target']) +def test_sys_init_fn_macos(dut: IdfDut) -> None: + dut.run_all_single_board_cases(timeout=60) diff --git a/components/esp_system/test_apps/sys_init_fn/sdkconfig.defaults b/components/esp_system/test_apps/sys_init_fn/sdkconfig.defaults new file mode 100644 index 0000000000..9b39f10b99 --- /dev/null +++ b/components/esp_system/test_apps/sys_init_fn/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_IDF_TARGET="linux" diff --git a/components/freertos/FreeRTOS-Kernel/portable/linux/port_idf.c b/components/freertos/FreeRTOS-Kernel/portable/linux/port_idf.c index 98db411470..c476decd76 100644 --- a/components/freertos/FreeRTOS-Kernel/portable/linux/port_idf.c +++ b/components/freertos/FreeRTOS-Kernel/portable/linux/port_idf.c @@ -98,7 +98,7 @@ static void main_task(void* args) vTaskDelete(NULL); } -int main(int argc, const char **argv) +void esp_startup_start_app(void) { // This makes sure that stdio is always synchronized so that idf.py monitor // and other tools read text output on time. diff --git a/tools/mocks/freertos/CMakeLists.txt b/tools/mocks/freertos/CMakeLists.txt index 9286bd3d2c..8cf8adddf9 100644 --- a/tools/mocks/freertos/CMakeLists.txt +++ b/tools/mocks/freertos/CMakeLists.txt @@ -30,3 +30,7 @@ idf_component_mock(INCLUDE_DIRS ${include_dirs} idf_component_get_property(freertos_lib freertos COMPONENT_LIB) target_compile_definitions(${freertos_lib} PUBLIC "projCOVERAGE_TEST=0") + +# When using FreeRTOS mocks, prevent esp_system from providing main() so tests can provide their own +idf_component_get_property(esp_system_lib esp_system COMPONENT_LIB) +target_compile_definitions(${esp_system_lib} PRIVATE ESP_SYSTEM_LINUX_NO_MAIN) diff --git a/tools/mocks/freertos/Kconfig b/tools/mocks/freertos/Kconfig index 0a2da0b01b..02c61bca19 100644 --- a/tools/mocks/freertos/Kconfig +++ b/tools/mocks/freertos/Kconfig @@ -44,6 +44,7 @@ menu "FreeRTOS" # Linux FreeRTOS port supports single-core only. bool default y + select ESP_SYSTEM_SINGLE_CORE_MODE config FREERTOS_NUMBER_OF_CORES # Invisible option to configure the number of cores on which FreeRTOS runs