mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 03:00:34 +03:00
Merge branch 'feat/error-code-registration' into 'master'
feat(esp_common): implement composable error code registration via link-time arrays Closes IDF-15105 and IDF-15486 See merge request espressif/esp-idf!46125
This commit is contained in:
Executable
+146
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Pre-commit check: ensure every header defining ESP_ERR_* codes is registered
|
||||
via idf_define_esp_err_codes() in its component's CMakeLists.txt.
|
||||
|
||||
Compares the set of headers found by scanning all components against those
|
||||
explicitly listed in idf_define_esp_err_codes(HEADERS ...) calls.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Add tools/ to path so we can import err_codes_extract
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
from err_codes_extract import EXCLUDE_SEARCH_DIRS
|
||||
from err_codes_extract import extract_from_file
|
||||
|
||||
# Additional directories to exclude beyond those in err_codes_extract
|
||||
EXTRA_EXCLUDE_DIRS = {'remote'}
|
||||
|
||||
# Pattern matching idf_define_esp_err_codes(HEADERS ...) in CMakeLists.txt
|
||||
ERR_CODES_CALL = re.compile(r'idf_define_esp_err_codes\s*\(')
|
||||
|
||||
|
||||
def find_all_err_headers(components_dir: str) -> dict[str, set[str]]:
|
||||
"""
|
||||
Scan all component headers for ESP_ERR_* definitions using
|
||||
err_codes_extract.extract_from_file().
|
||||
|
||||
Returns:
|
||||
dict mapping component name -> set of header paths (relative to component dir)
|
||||
"""
|
||||
exclude = EXCLUDE_SEARCH_DIRS | EXTRA_EXCLUDE_DIRS
|
||||
result: dict[str, set[str]] = {}
|
||||
|
||||
for component_name in sorted(os.listdir(components_dir)):
|
||||
component_dir = os.path.join(components_dir, component_name)
|
||||
if not os.path.isdir(component_dir):
|
||||
continue
|
||||
|
||||
for root, dirnames, filenames in os.walk(component_dir, topdown=True):
|
||||
dirnames[:] = [d for d in dirnames if d not in exclude]
|
||||
for filename in filenames:
|
||||
if not filename.endswith('.h'):
|
||||
continue
|
||||
filepath = os.path.join(root, filename)
|
||||
rel_path = os.path.relpath(filepath, component_dir)
|
||||
if extract_from_file(filepath, rel_path):
|
||||
result.setdefault(component_name, set()).add(rel_path)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_registered_headers(components_dir: str) -> dict[str, set[str]]:
|
||||
"""
|
||||
Parse all CMakeLists.txt files for idf_define_esp_err_codes(HEADERS ...) calls.
|
||||
|
||||
Returns:
|
||||
dict mapping component name -> set of header paths (relative to component dir)
|
||||
"""
|
||||
result: dict[str, set[str]] = {}
|
||||
|
||||
for component_name in sorted(os.listdir(components_dir)):
|
||||
component_dir = os.path.join(components_dir, component_name)
|
||||
cmake_file = os.path.join(component_dir, 'CMakeLists.txt')
|
||||
if not os.path.isfile(cmake_file):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(cmake_file, encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except (UnicodeDecodeError, OSError):
|
||||
continue
|
||||
|
||||
# Find all idf_define_esp_err_codes() calls and extract HEADERS args
|
||||
for match in ERR_CODES_CALL.finditer(content):
|
||||
start = match.end()
|
||||
# Find the matching closing paren, handling nesting
|
||||
depth = 1
|
||||
pos = start
|
||||
while pos < len(content) and depth > 0:
|
||||
if content[pos] == '(':
|
||||
depth += 1
|
||||
elif content[pos] == ')':
|
||||
depth -= 1
|
||||
pos += 1
|
||||
|
||||
args_str = content[start : pos - 1]
|
||||
# Remove comments
|
||||
args_str = re.sub(r'#.*$', '', args_str, flags=re.MULTILINE)
|
||||
# Extract tokens after HEADERS keyword
|
||||
headers_match = re.search(r'\bHEADERS\b\s+(.*)', args_str, re.DOTALL)
|
||||
if headers_match:
|
||||
tokens = headers_match.group(1).split()
|
||||
# Stop at next keyword (uppercase word) or end
|
||||
for token in tokens:
|
||||
if re.match(r'^[A-Z_]+$', token):
|
||||
break
|
||||
# Normalize path separators
|
||||
header_path = token.replace('\\', '/')
|
||||
result.setdefault(component_name, set()).add(header_path)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
idf_path = os.environ.get('IDF_PATH', os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
components_dir = os.path.join(idf_path, 'components')
|
||||
|
||||
if not os.path.isdir(components_dir):
|
||||
print(f'Error: components directory not found: {components_dir}', file=sys.stderr)
|
||||
return 1
|
||||
|
||||
found_headers = find_all_err_headers(components_dir)
|
||||
registered_headers = parse_registered_headers(components_dir)
|
||||
|
||||
errors: list[str] = []
|
||||
|
||||
for component, headers in sorted(found_headers.items()):
|
||||
registered = registered_headers.get(component, set())
|
||||
missing = headers - registered
|
||||
if missing:
|
||||
for header in sorted(missing):
|
||||
errors.append(
|
||||
f'{component}: {header} defines ESP_ERR_* codes but is not registered in idf_define_esp_err_codes()'
|
||||
)
|
||||
|
||||
if errors:
|
||||
print('Error code registration check failed:', file=sys.stderr)
|
||||
for error in errors:
|
||||
print(f' {error}', file=sys.stderr)
|
||||
print(
|
||||
f'\n{len(errors)} header(s) define ESP_ERR_* codes but are not registered.\n'
|
||||
f"Add idf_define_esp_err_codes(HEADERS ...) to the component's CMakeLists.txt.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -57,6 +57,7 @@ tools/ci/check_api_violation.sh
|
||||
tools/ci/check_build_test_rules.py
|
||||
tools/ci/check_callgraph.py
|
||||
tools/ci/check_codeowners.py
|
||||
tools/ci/check_err_codes_registration.py
|
||||
tools/ci/check_esp_memory_utils_headers.sh
|
||||
tools/ci/check_examples_extra_component_dirs.sh
|
||||
tools/ci/check_executables.py
|
||||
|
||||
@@ -41,7 +41,6 @@ tools/esp_app_trace/espytrace/apptrace.py
|
||||
tools/esp_app_trace/espytrace/sysview.py
|
||||
tools/esp_app_trace/logtrace_proc.py
|
||||
tools/esp_app_trace/sysviewtrace_proc.py
|
||||
tools/gen_esp_err_to_name.py
|
||||
tools/gen_soc_caps_kconfig/test/test_gen_soc_caps_kconfig.py
|
||||
tools/ldgen/ldgen.py
|
||||
tools/ldgen/ldgen/entity.py
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# CMake module for composable error code registration.
|
||||
#
|
||||
# Provides the idf_define_esp_err_codes() function that components call
|
||||
# to register their error codes into a link-time array.
|
||||
#
|
||||
|
||||
#
|
||||
# idf_define_esp_err_codes
|
||||
#
|
||||
# Register error codes defined in the specified header files.
|
||||
# This function:
|
||||
# 1. Runs err_codes_extract.py to extract error codes from headers into a CSV
|
||||
# 2. Runs err_codes_to_c.py to generate a C source file placing entries into
|
||||
# the .esp_err_msg_tbl linker section
|
||||
# 3. Adds the generated C source file to the current component
|
||||
# 4. Adds -Wl,--undefined to force-link the error code symbols
|
||||
#
|
||||
# Usage (must be called AFTER idf_component_register):
|
||||
# idf_define_esp_err_codes(HEADERS include/my_errors.h include/other.h)
|
||||
#
|
||||
# Note: This function relies on COMPONENT_LIB and COMPONENT_NAME which are
|
||||
# only available after idf_component_register() has been called.
|
||||
#
|
||||
function(idf_define_esp_err_codes)
|
||||
# If error-to-name lookup is disabled, do nothing
|
||||
if(NOT CONFIG_ESP_ERR_TO_NAME_LOOKUP)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(options "")
|
||||
set(single_value "")
|
||||
set(multi_value HEADERS)
|
||||
cmake_parse_arguments(ARG "${options}" "${single_value}" "${multi_value}" ${ARGN})
|
||||
|
||||
idf_build_get_property(idf_path IDF_PATH)
|
||||
idf_build_get_property(build_dir BUILD_DIR)
|
||||
idf_build_get_property(python PYTHON)
|
||||
|
||||
# Determine component name
|
||||
if(NOT COMPONENT_NAME)
|
||||
get_filename_component(COMPONENT_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
endif()
|
||||
|
||||
# Collect header files
|
||||
set(header_files "")
|
||||
|
||||
# Process explicit headers (relative to component dir)
|
||||
foreach(header ${ARG_HEADERS})
|
||||
if(IS_ABSOLUTE ${header})
|
||||
list(APPEND header_files "${header}")
|
||||
else()
|
||||
list(APPEND header_files "${CMAKE_CURRENT_LIST_DIR}/${header}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(NOT header_files)
|
||||
message(FATAL_ERROR "idf_define_esp_err_codes: No header files found for component ${COMPONENT_NAME}")
|
||||
endif()
|
||||
|
||||
# Output paths
|
||||
set(csv_output "${build_dir}/esp_err_codes/${COMPONENT_NAME}_err_codes.csv")
|
||||
set(c_output "${build_dir}/esp_err_codes/${COMPONENT_NAME}_esp_err_codes.c")
|
||||
|
||||
# Ensure output directory exists
|
||||
file(MAKE_DIRECTORY "${build_dir}/esp_err_codes")
|
||||
|
||||
# Build the header arguments for the extract script
|
||||
# Always provide esp_err.h as a context header for ESP_ERR_*_BASE definitions
|
||||
set(header_args "")
|
||||
set(esp_err_h "${idf_path}/components/esp_common/include/esp_err.h")
|
||||
set(context_args "")
|
||||
set(need_context TRUE)
|
||||
foreach(h ${header_files})
|
||||
list(APPEND header_args "${h}")
|
||||
if("${h}" STREQUAL "${esp_err_h}")
|
||||
set(need_context FALSE)
|
||||
endif()
|
||||
endforeach()
|
||||
if(need_context)
|
||||
set(context_args "--context-headers" "${esp_err_h}")
|
||||
endif()
|
||||
|
||||
# Step 1: Extract error codes from headers to CSV (build time)
|
||||
set(extract_script "${idf_path}/tools/err_codes_extract.py")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${csv_output}"
|
||||
COMMAND ${python} "${extract_script}"
|
||||
--headers ${header_args}
|
||||
${context_args}
|
||||
--base-path "${idf_path}"
|
||||
--output "${csv_output}"
|
||||
DEPENDS ${header_files} ${esp_err_h} "${extract_script}"
|
||||
COMMENT "Extracting error codes for ${COMPONENT_NAME}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
# Compute include-form headers for the generated C file
|
||||
set(include_headers "")
|
||||
foreach(h ${header_files})
|
||||
# Convert full path to include-relative path
|
||||
get_filename_component(h_name "${h}" NAME)
|
||||
# Try to find 'include' in the path
|
||||
string(FIND "${h}" "/include/" include_pos)
|
||||
if(include_pos GREATER -1)
|
||||
math(EXPR include_start "${include_pos} + 9") # length of "/include/"
|
||||
string(SUBSTRING "${h}" ${include_start} -1 include_rel)
|
||||
list(APPEND include_headers "${include_rel}")
|
||||
else()
|
||||
list(APPEND include_headers "${h_name}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Step 2: Generate C source from CSV (build time)
|
||||
set(gen_script "${idf_path}/tools/err_codes_to_c.py")
|
||||
set(header_flags "")
|
||||
foreach(inc ${include_headers})
|
||||
list(APPEND header_flags "--headers" "${inc}")
|
||||
endforeach()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${c_output}"
|
||||
COMMAND ${python} "${gen_script}"
|
||||
--csv "${csv_output}"
|
||||
--output "${c_output}"
|
||||
--component "${COMPONENT_NAME}"
|
||||
${header_flags}
|
||||
DEPENDS "${csv_output}" "${gen_script}"
|
||||
COMMENT "Generating error code table for ${COMPONENT_NAME}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
# Step 3: Add generated C source to the component
|
||||
target_sources(${COMPONENT_LIB} PRIVATE "${c_output}")
|
||||
|
||||
# Step 4: Force-link the error codes symbol so it's not discarded
|
||||
string(REPLACE "-" "_" safe_component "${COMPONENT_NAME}")
|
||||
string(REPLACE "." "_" safe_component "${safe_component}")
|
||||
# On macOS (Mach-O) the linker sees C symbols with a leading '_' prepended
|
||||
# by the compiler. Pass the mangled name so -u resolves correctly.
|
||||
# On Linux (ELF) no prefix is added, so use the C identifier as-is.
|
||||
if(APPLE)
|
||||
set(force_link_symbol "_esp_err_msg_${safe_component}_include")
|
||||
else()
|
||||
set(force_link_symbol "esp_err_msg_${safe_component}_include")
|
||||
endif()
|
||||
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE "-u ${force_link_symbol}")
|
||||
|
||||
endfunction()
|
||||
@@ -51,6 +51,7 @@ if(NOT __idf_env_set)
|
||||
include(gdbinit)
|
||||
include(prefix_map)
|
||||
include(openocd)
|
||||
include(err_codes)
|
||||
|
||||
# ESP-IDF extra dependencies defined in tools/idf_extra_components.yml
|
||||
if(WIN32)
|
||||
|
||||
@@ -831,13 +831,29 @@ macro(project project_name)
|
||||
COMMAND ${CMAKE_COMMAND} -E touch ${project_elf_src}
|
||||
VERBATIM)
|
||||
add_custom_target(_project_elf_src DEPENDS "${project_elf_src}")
|
||||
|
||||
# On the Linux (host) target the standard GNU ld processes static archives
|
||||
# in a single left-to-right pass, which fails when component libraries (or
|
||||
# their transitive dependencies such as the mbedtls sub-libraries) have
|
||||
# circular symbol references. Wrap all archives in --start-group /
|
||||
# --end-group so the linker re-scans until every symbol is resolved.
|
||||
if(CONFIG_IDF_TARGET_LINUX AND NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
|
||||
string(CONCAT _link_exe_template
|
||||
"<CMAKE_C_COMPILER> <FLAGS> <CMAKE_C_LINK_FLAGS> <LINK_FLAGS>"
|
||||
" <OBJECTS> -o <TARGET>"
|
||||
" -Wl,--start-group <LINK_LIBRARIES> -Wl,--end-group")
|
||||
set(CMAKE_C_LINK_EXECUTABLE "${_link_exe_template}")
|
||||
string(REPLACE "<CMAKE_C_COMPILER>" "<CMAKE_CXX_COMPILER>"
|
||||
_link_exe_template "${_link_exe_template}")
|
||||
string(REPLACE "<CMAKE_C_LINK_FLAGS>" "<CMAKE_CXX_LINK_FLAGS>"
|
||||
_link_exe_template "${_link_exe_template}")
|
||||
set(CMAKE_CXX_LINK_EXECUTABLE "${_link_exe_template}")
|
||||
unset(_link_exe_template)
|
||||
endif()
|
||||
|
||||
add_executable(${project_elf} "${project_elf_src}")
|
||||
add_dependencies(${project_elf} _project_elf_src)
|
||||
|
||||
if(__PROJECT_GROUP_LINK_COMPONENTS)
|
||||
target_link_libraries(${project_elf} PRIVATE "-Wl,--start-group")
|
||||
endif()
|
||||
|
||||
if(CONFIG_IDF_TARGET_LINUX AND CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
|
||||
# Compiling for the host, and the host is macOS, so the linker is Darwin LD.
|
||||
# Note, when adding support for Clang and LLD based toolchain this check will
|
||||
@@ -847,6 +863,10 @@ macro(project project_name)
|
||||
set(linker_type "GNU")
|
||||
endif()
|
||||
|
||||
if(__PROJECT_GROUP_LINK_COMPONENTS)
|
||||
target_link_libraries(${project_elf} PRIVATE "-Wl,--start-group")
|
||||
endif()
|
||||
|
||||
if(test_components)
|
||||
if(linker_type STREQUAL "GNU")
|
||||
target_link_libraries(${project_elf} PRIVATE "-Wl,--whole-archive")
|
||||
|
||||
@@ -32,6 +32,10 @@ include(${CMAKE_CURRENT_LIST_DIR}/../cmake/openocd.cmake)
|
||||
# idf_build_generate_depgraph function.
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/depgraph.cmake)
|
||||
|
||||
# The err_codes.cmake file from cmakev1 provides idf_define_esp_err_codes(),
|
||||
# which generates per-component error code tables placed in a link-time array.
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/err_codes.cmake)
|
||||
|
||||
include(component)
|
||||
include(build)
|
||||
include(kconfig)
|
||||
@@ -42,6 +46,7 @@ include(ldgen)
|
||||
include(dfu)
|
||||
include(uf2)
|
||||
include(size)
|
||||
|
||||
include(GetGitRevisionDescription)
|
||||
# For backward compatibility, since externalproject_add is used by
|
||||
# project_include.cmake in the bootloader component. The ExternalProject
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Extract ESP_ERR_* error code definitions from C header files and output them as CSV.
|
||||
# This script is used by the composable error code registration system.
|
||||
#
|
||||
# Usage:
|
||||
# python err_codes_extract.py --headers file1.h file2.h --output errors.csv
|
||||
# python err_codes_extract.py --search-dirs components/ --output errors.csv
|
||||
#
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import fnmatch
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# Directories to exclude from recursive search
|
||||
EXCLUDE_SEARCH_DIRS = {'test_apps', 'unit-test-app', 'CMock', 'test'}
|
||||
|
||||
# Files to exclude
|
||||
EXCLUDE_FILES: list[str] = []
|
||||
|
||||
|
||||
class ErrCode:
|
||||
"""Represents a single extracted error code definition."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
file: str,
|
||||
value: int | None = None,
|
||||
base_name: str = '',
|
||||
base_offset: int = 0,
|
||||
comment: str = '',
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.file = file
|
||||
self.value = value # Resolved integer value, or None if unresolved
|
||||
self.base_name = base_name # Name of base error if this is BASE + offset
|
||||
self.base_offset = base_offset
|
||||
self.comment = comment
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.value is not None:
|
||||
return f'{self.name} = {self.value} (0x{self.value & 0xFFFFFFFF:x}) from {self.file}'
|
||||
return f'{self.name} = {self.base_name}+{self.base_offset} from {self.file}'
|
||||
|
||||
|
||||
def _parse_define_value(value_str: str) -> tuple[int | None, str, int]:
|
||||
"""
|
||||
Parse the value part of a #define.
|
||||
|
||||
Returns:
|
||||
(resolved_value, base_name, base_offset)
|
||||
If the value is a plain number, resolved_value is set and base_name is empty.
|
||||
If the value references a base, resolved_value is None and base_name/base_offset are set.
|
||||
"""
|
||||
# Strip surrounding parentheses
|
||||
value_str = value_str.strip()
|
||||
m = re.match(r'^\((.+)\)$', value_str)
|
||||
if m:
|
||||
value_str = m.group(1).strip()
|
||||
|
||||
# Check for BASE + offset pattern
|
||||
m = re.match(r'(\w+)\s*\+\s*(.+)', value_str)
|
||||
if m:
|
||||
base_name = m.group(1)
|
||||
offset_str = m.group(2).strip()
|
||||
# Try to parse offset as hex or decimal
|
||||
offset = _parse_number(offset_str)
|
||||
if offset is not None:
|
||||
return (None, base_name, offset)
|
||||
# offset might be another symbol — treat as unresolvable for now
|
||||
return (None, base_name, 0)
|
||||
|
||||
# Try to parse as a plain number
|
||||
num = _parse_number(value_str)
|
||||
if num is not None:
|
||||
return (num, '', 0)
|
||||
|
||||
# It might be a reference to another symbol (no offset)
|
||||
m = re.match(r'^(\w+)$', value_str)
|
||||
if m:
|
||||
return (None, m.group(1), 0)
|
||||
|
||||
return (None, '', 0)
|
||||
|
||||
|
||||
def _parse_number(s: str) -> int | None:
|
||||
"""Parse a hexadecimal or decimal integer from a string."""
|
||||
s = s.strip()
|
||||
m = re.match(r'^0x([0-9A-Fa-f]+)$', s)
|
||||
if m:
|
||||
return int(m.group(1), 16)
|
||||
m = re.match(r'^(-?[0-9]+)$', s)
|
||||
if m:
|
||||
return int(m.group(1), 10)
|
||||
return None
|
||||
|
||||
|
||||
def extract_from_file(filepath: str, rel_path: str) -> list[ErrCode]:
|
||||
"""
|
||||
Extract ESP_ERR_* defines from a single header file.
|
||||
|
||||
Args:
|
||||
filepath: Absolute path to the file.
|
||||
rel_path: Relative path for display/CSV output.
|
||||
|
||||
Returns:
|
||||
List of ErrCode objects.
|
||||
"""
|
||||
results: list[ErrCode] = []
|
||||
define_pattern = re.compile(
|
||||
r'^\s*#\s*define\s+(ESP_ERR_\w+|ESP_OK\w*|ESP_FAIL)\s+(.*?)(?:\s*/\*[*!]?<?\s*(.*?)\s*\*/)?(?:\s*/{2,3}\s*!?<?\s*(.*))?$'
|
||||
)
|
||||
|
||||
try:
|
||||
with open(filepath, encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.rstrip('\n')
|
||||
m = define_pattern.match(line)
|
||||
if not m:
|
||||
continue
|
||||
|
||||
name = m.group(1)
|
||||
value_str = m.group(2).strip()
|
||||
comment = (m.group(3) or m.group(4) or '').strip()
|
||||
|
||||
# Remove trailing inline comments from value_str
|
||||
# e.g. "0x101 /*!< Out of memory */" -> "0x101"
|
||||
comment_match = re.search(r'\s*/\*[*!]?<?\s*(.*?)\s*\*/', value_str)
|
||||
if comment_match:
|
||||
if not comment:
|
||||
comment = comment_match.group(1).strip()
|
||||
value_str = value_str[: comment_match.start()].strip()
|
||||
|
||||
# Handle multi-line comments that start with /*!< but don't close on same line
|
||||
comment_match_open = re.search(r'\s*/\*[*!]?<?\s*(.*)', value_str)
|
||||
if comment_match_open:
|
||||
if not comment:
|
||||
comment = comment_match_open.group(1).strip().rstrip(',')
|
||||
value_str = value_str[: comment_match_open.start()].strip()
|
||||
|
||||
# Also handle // and /// comments at end
|
||||
comment_match2 = re.search(r'\s*/{2,3}\s*!?<?\s*(.*)', value_str)
|
||||
if comment_match2:
|
||||
if not comment:
|
||||
comment = comment_match2.group(1).strip()
|
||||
value_str = value_str[: comment_match2.start()].strip()
|
||||
|
||||
resolved, base_name, base_offset = _parse_define_value(value_str)
|
||||
results.append(
|
||||
ErrCode(
|
||||
name=name,
|
||||
file=rel_path,
|
||||
value=resolved,
|
||||
base_name=base_name,
|
||||
base_offset=base_offset,
|
||||
comment=comment,
|
||||
)
|
||||
)
|
||||
except UnicodeDecodeError:
|
||||
print(f'Warning: Cannot decode {filepath}, skipping', file=sys.stderr)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def resolve_error_codes(err_codes: list[ErrCode]) -> list[ErrCode]:
|
||||
"""
|
||||
Resolve error codes that depend on base codes.
|
||||
Performs multiple passes to handle transitive dependencies.
|
||||
|
||||
Returns list of resolved ErrCode objects (with .value set).
|
||||
Unresolvable codes are printed as warnings and excluded.
|
||||
"""
|
||||
# Build a lookup from name to resolved value
|
||||
resolved: dict[str, int] = {}
|
||||
unresolved: list[ErrCode] = []
|
||||
|
||||
for ec in err_codes:
|
||||
if ec.value is not None:
|
||||
resolved[ec.name] = ec.value
|
||||
else:
|
||||
unresolved.append(ec)
|
||||
|
||||
# Iteratively resolve
|
||||
max_iterations = 10
|
||||
for _ in range(max_iterations):
|
||||
still_unresolved = []
|
||||
for ec in unresolved:
|
||||
if ec.base_name in resolved:
|
||||
ec.value = resolved[ec.base_name] + ec.base_offset
|
||||
resolved[ec.name] = ec.value
|
||||
else:
|
||||
still_unresolved.append(ec)
|
||||
if len(still_unresolved) == len(unresolved):
|
||||
break # No progress
|
||||
unresolved = still_unresolved
|
||||
|
||||
for ec in unresolved:
|
||||
print(f'Warning: Cannot resolve {ec.name} (depends on {ec.base_name}) in {ec.file}', file=sys.stderr)
|
||||
|
||||
return [ec for ec in err_codes if ec.value is not None]
|
||||
|
||||
|
||||
def search_headers(search_dirs: list[str], base_path: str) -> list[str]:
|
||||
"""
|
||||
Recursively find all .h files in the given directories,
|
||||
excluding test directories.
|
||||
"""
|
||||
headers: list[str] = []
|
||||
for search_dir in search_dirs:
|
||||
full_dir = os.path.join(base_path, search_dir) if not os.path.isabs(search_dir) else search_dir
|
||||
for root, dirnames, filenames in os.walk(full_dir, topdown=True):
|
||||
# Filter out excluded directories
|
||||
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_SEARCH_DIRS]
|
||||
for filename in fnmatch.filter(filenames, '*.h'):
|
||||
headers.append(os.path.join(root, filename))
|
||||
return headers
|
||||
|
||||
|
||||
def extract_all(headers: list[str], base_path: str, context_headers: list[str] | None = None) -> list[ErrCode]:
|
||||
"""
|
||||
Extract and resolve error codes from a list of header files.
|
||||
|
||||
Args:
|
||||
headers: Headers whose error codes will be emitted.
|
||||
base_path: Base path for relative paths.
|
||||
context_headers: Additional headers scanned only for base definitions
|
||||
(e.g. ESP_ERR_*_BASE). Their codes are used for
|
||||
resolution but are excluded from the final output.
|
||||
"""
|
||||
context_names: set = set()
|
||||
all_codes: list[ErrCode] = []
|
||||
|
||||
# Scan context headers first — their defines help resolve bases
|
||||
if context_headers:
|
||||
for header in context_headers:
|
||||
rel = os.path.relpath(header, base_path) if base_path else header
|
||||
ctx_codes = extract_from_file(header, rel)
|
||||
for ec in ctx_codes:
|
||||
context_names.add(ec.name)
|
||||
all_codes.extend(ctx_codes)
|
||||
|
||||
# Scan primary headers
|
||||
for header in headers:
|
||||
rel = os.path.relpath(header, base_path) if base_path else header
|
||||
all_codes.extend(extract_from_file(header, rel))
|
||||
|
||||
resolved = resolve_error_codes(all_codes)
|
||||
|
||||
# Remove context-only codes from the output
|
||||
if context_names:
|
||||
resolved = [ec for ec in resolved if ec.name not in context_names]
|
||||
|
||||
return resolved
|
||||
|
||||
|
||||
def validate_error_codes(err_codes: list[ErrCode]) -> list[str]:
|
||||
"""
|
||||
Validate extracted error codes and return a list of error messages.
|
||||
Returns an empty list if all checks pass.
|
||||
|
||||
Checks performed:
|
||||
- No two non-BASE error codes share the same numeric value.
|
||||
- No duplicate names with conflicting values.
|
||||
"""
|
||||
errors: list[str] = []
|
||||
|
||||
# --- Check 1: duplicate values (skip _BASE codes, they are offset anchors) ---
|
||||
value_to_codes: dict[int, list[ErrCode]] = {}
|
||||
for ec in err_codes:
|
||||
if ec.value is None or ec.name.endswith('_BASE'):
|
||||
continue
|
||||
value_to_codes.setdefault(ec.value, []).append(ec)
|
||||
|
||||
for value, codes in sorted(value_to_codes.items()):
|
||||
# Deduplicate by name — same name from different files is OK
|
||||
unique_names = {ec.name for ec in codes}
|
||||
if len(unique_names) > 1:
|
||||
names_str = ', '.join(f'{ec.name} ({ec.file})' for ec in codes)
|
||||
errors.append(f'Duplicate value 0x{value & 0xFFFFFFFF:x} ({value}): {names_str}')
|
||||
|
||||
# --- Check 2: same name defined with different values ---
|
||||
name_to_values: dict[str, list[ErrCode]] = {}
|
||||
for ec in err_codes:
|
||||
if ec.value is None:
|
||||
continue
|
||||
name_to_values.setdefault(ec.name, []).append(ec)
|
||||
|
||||
for name, codes in sorted(name_to_values.items()):
|
||||
unique_values = {ec.value for ec in codes}
|
||||
if len(unique_values) > 1:
|
||||
defs_str = ', '.join(f'{ec.value} in {ec.file}' for ec in codes)
|
||||
errors.append(f'Conflicting definitions for {name}: {defs_str}')
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def write_csv(err_codes: list[ErrCode], output_path: str) -> None:
|
||||
"""Write resolved error codes to a CSV file."""
|
||||
# Deduplicate: keep the first occurrence of each name
|
||||
seen = set()
|
||||
unique_codes: list[ErrCode] = []
|
||||
for ec in err_codes:
|
||||
if ec.name not in seen:
|
||||
seen.add(ec.name)
|
||||
unique_codes.append(ec)
|
||||
|
||||
# Sort by value then name
|
||||
unique_codes.sort(key=lambda e: (e.value if e.value is not None else 0, e.name))
|
||||
|
||||
with open(output_path, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['name', 'value', 'file', 'comment'])
|
||||
for ec in unique_codes:
|
||||
writer.writerow([ec.name, ec.value, ec.file, ec.comment])
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description='Extract ESP error codes from header files and output as CSV')
|
||||
parser.add_argument('--headers', nargs='*', default=[], help='Specific header files to process')
|
||||
parser.add_argument(
|
||||
'--context-headers',
|
||||
nargs='*',
|
||||
default=[],
|
||||
help='Header files scanned only for base definitions (e.g. ESP_ERR_*_BASE). '
|
||||
'Codes found here are used for resolution but not emitted.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--search-dirs', nargs='*', default=[], help='Directories to search recursively for header files'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--base-path', default=None, help='Base path for computing relative paths (default: IDF_PATH or cwd)'
|
||||
)
|
||||
parser.add_argument('--output', required=True, help='Output CSV file path')
|
||||
parser.add_argument(
|
||||
'--validate',
|
||||
action='store_true',
|
||||
help='Validate error codes (fail on duplicate values or conflicting definitions)',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
base_path = args.base_path
|
||||
if base_path is None:
|
||||
base_path = os.environ.get('IDF_PATH', os.getcwd())
|
||||
|
||||
headers = list(args.headers)
|
||||
|
||||
if args.search_dirs:
|
||||
headers.extend(search_headers(args.search_dirs, base_path))
|
||||
|
||||
if not headers:
|
||||
print('Error: No header files specified. Use --headers or --search-dirs.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
context_headers = list(args.context_headers) if args.context_headers else None
|
||||
|
||||
err_codes = extract_all(headers, base_path, context_headers=context_headers)
|
||||
write_csv(err_codes, args.output)
|
||||
|
||||
if args.validate:
|
||||
validation_errors = validate_error_codes(err_codes)
|
||||
if validation_errors:
|
||||
print(f'\nValidation FAILED with {len(validation_errors)} error(s):', file=sys.stderr)
|
||||
for err in validation_errors:
|
||||
print(f' ERROR: {err}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print('Validation passed: no duplicate values or conflicting definitions.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,142 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Generate a C source file that registers error codes into a designated linker section.
|
||||
# The generated file creates an array of esp_err_msg_t entries in the ".esp_err_msg_tbl"
|
||||
# section, which is collected at link time by the linker script.
|
||||
#
|
||||
# Usage:
|
||||
# python err_codes_to_c.py --csv errors.csv --output esp_err_codes_mycomp.c --component mycomp
|
||||
#
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import datetime
|
||||
import os
|
||||
|
||||
|
||||
def read_csv(csv_path: str) -> list[tuple[str, int, str]]:
|
||||
"""
|
||||
Read error codes from CSV file.
|
||||
Returns list of (name, value, comment) tuples.
|
||||
"""
|
||||
entries: list[tuple[str, int, str]] = []
|
||||
with open(csv_path, encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
name = row['name']
|
||||
value = int(row['value'])
|
||||
comment = row.get('comment', '')
|
||||
entries.append((name, value, comment))
|
||||
return entries
|
||||
|
||||
|
||||
def generate_c_source(entries: list[tuple[str, int, str]], component: str, headers: list[str]) -> str:
|
||||
"""
|
||||
Generate C source code that places error code entries into the
|
||||
.esp_err_msg_tbl linker section.
|
||||
"""
|
||||
lines: list[str] = []
|
||||
year = datetime.datetime.now().year
|
||||
lines.append('/*')
|
||||
lines.append(f' * SPDX-FileCopyrightText: {year} Espressif Systems (Shanghai) CO LTD')
|
||||
lines.append(' *')
|
||||
lines.append(' * SPDX-License-Identifier: Apache-2.0')
|
||||
lines.append(' */')
|
||||
lines.append('')
|
||||
lines.append('// Auto-generated by err_codes_to_c.py — do not edit')
|
||||
lines.append('')
|
||||
lines.append('#include "esp_err.h"')
|
||||
lines.append('#include "esp_err_msg.h"')
|
||||
lines.append('#include "esp_attr.h"')
|
||||
lines.append('')
|
||||
|
||||
# Include the headers that define the error codes
|
||||
# Skip headers already included above
|
||||
already_included = {'esp_err.h', 'esp_err_msg.h', 'esp_attr.h'}
|
||||
for header in headers:
|
||||
if header in already_included:
|
||||
continue
|
||||
lines.append(f'#if __has_include("{header}")')
|
||||
lines.append(f'#include "{header}"')
|
||||
lines.append('#endif')
|
||||
lines.append('')
|
||||
|
||||
lines.append('#ifdef CONFIG_ESP_ERR_TO_NAME_LOOKUP')
|
||||
lines.append('')
|
||||
|
||||
# Generate array entries with #ifdef guards
|
||||
safe_component = component.replace('-', '_').replace('.', '_')
|
||||
symbol_name = f'esp_err_msg_{safe_component}'
|
||||
|
||||
lines.append(f'// Force-link symbol for {component} error codes')
|
||||
lines.append(f'const int {symbol_name}_include = 0;')
|
||||
lines.append('')
|
||||
|
||||
if entries:
|
||||
lines.append(f'static const esp_err_msg_t {symbol_name}[]')
|
||||
lines.append(' PLACE_IN_SECTION("esp_err_msg_tbl") = {')
|
||||
|
||||
for name, value, comment in entries:
|
||||
comment_str = f' /* {comment} */' if comment else ''
|
||||
lines.append(f'#ifdef {name}')
|
||||
lines.append(f' {{ {name}, "{name}" }},{comment_str}')
|
||||
lines.append('#endif')
|
||||
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
lines.append('#endif // CONFIG_ESP_ERR_TO_NAME_LOOKUP')
|
||||
lines.append('')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def _infer_include_path(file_path: str) -> str:
|
||||
"""
|
||||
Convert a file path like 'components/foo/include/bar/baz.h'
|
||||
into an include path like 'bar/baz.h'.
|
||||
"""
|
||||
parts = file_path.replace('\\', '/').split('/')
|
||||
try:
|
||||
idx = parts.index('include')
|
||||
return '/'.join(parts[idx + 1 :])
|
||||
except ValueError:
|
||||
return os.path.basename(file_path)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate C source file with error code registration via linker section'
|
||||
)
|
||||
parser.add_argument('--csv', required=True, help='Input CSV file with error codes (from err_codes_extract.py)')
|
||||
parser.add_argument('--output', required=True, help='Output C source file path')
|
||||
parser.add_argument('--component', required=True, help='Component name (used for symbol naming)')
|
||||
parser.add_argument('--headers', nargs='*', default=[], help='Header files to #include (include-path form)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
entries = read_csv(args.csv)
|
||||
|
||||
# Deduce headers from the CSV file paths if none specified
|
||||
headers = list(args.headers)
|
||||
if not headers:
|
||||
seen_files = set()
|
||||
with open(args.csv, encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
file_path = row.get('file', '')
|
||||
if file_path and file_path not in seen_files:
|
||||
seen_files.add(file_path)
|
||||
inc = _infer_include_path(file_path)
|
||||
if inc not in headers:
|
||||
headers.append(inc)
|
||||
|
||||
source = generate_c_source(entries, args.component, headers)
|
||||
|
||||
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
f.write(source)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,98 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Generate RST documentation from extracted error codes.
|
||||
# This script takes a CSV file (produced by err_codes_extract.py) and generates
|
||||
# an RST file suitable for inclusion in Sphinx documentation.
|
||||
#
|
||||
# Usage:
|
||||
# python err_codes_to_rst.py --csv errors.csv --output error_codes.inc
|
||||
# python err_codes_to_rst.py --search-dirs components/ --output error_codes.inc
|
||||
#
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Allow importing err_codes_extract from same directory
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from err_codes_extract import extract_all # noqa: E402
|
||||
from err_codes_extract import search_headers # noqa: E402
|
||||
|
||||
|
||||
def read_csv_entries(csv_path: str) -> list[tuple[str, int, str]]:
|
||||
"""Read error codes from a CSV file."""
|
||||
entries: list[tuple[str, int, str]] = []
|
||||
with open(csv_path, encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
name = row['name']
|
||||
value = int(row['value'])
|
||||
comment = row.get('comment', '')
|
||||
entries.append((name, value, comment))
|
||||
return entries
|
||||
|
||||
|
||||
def generate_rst(entries: list[tuple[str, int, str]]) -> str:
|
||||
"""Generate RST content from error code entries.
|
||||
|
||||
Output format matches the existing esp_err_defs.inc generated by
|
||||
gen_esp_err_to_name.py so it can be used as a drop-in replacement
|
||||
in the docs build (included via ``.. include-build-file::``).
|
||||
"""
|
||||
lines: list[str] = []
|
||||
|
||||
# Sort by value: negative values first (ascending), then non-negative (ascending)
|
||||
sorted_entries = sorted(entries, key=lambda e: (e[1], e[0]))
|
||||
|
||||
for name, value, comment in sorted_entries:
|
||||
line = f':c:macro:`{name}` '
|
||||
if value > 0:
|
||||
line += f'**(0x{value:x})**'
|
||||
else:
|
||||
line += f'({value:d})'
|
||||
if comment:
|
||||
line += f': {comment}'
|
||||
lines.append(line)
|
||||
lines.append('')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description='Generate RST documentation of ESP error codes')
|
||||
parser.add_argument('--csv', default=None, help='Input CSV file with error codes (from err_codes_extract.py)')
|
||||
parser.add_argument(
|
||||
'--search-dirs', nargs='*', default=[], help='Directories to search recursively for header files'
|
||||
)
|
||||
parser.add_argument('--base-path', default=None, help='Base path for computing relative paths')
|
||||
parser.add_argument('--output', required=True, help='Output RST file path')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
base_path = args.base_path
|
||||
if base_path is None:
|
||||
base_path = os.environ.get('IDF_PATH', os.getcwd())
|
||||
|
||||
if args.csv:
|
||||
entries = read_csv_entries(args.csv)
|
||||
elif args.search_dirs:
|
||||
headers = search_headers(args.search_dirs, base_path)
|
||||
err_codes = extract_all(headers, base_path)
|
||||
entries = [(ec.name, ec.value, ec.comment) for ec in err_codes if ec.value is not None]
|
||||
else:
|
||||
print('Error: Specify either --csv or --search-dirs.', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
rst_content = generate_rst(entries)
|
||||
|
||||
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
f.write(rst_content)
|
||||
|
||||
print(f'Generated {args.output} with {len(entries)} error codes')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+35
-351
@@ -2,305 +2,49 @@
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2018-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Generate RST documentation of ESP error codes.
|
||||
#
|
||||
# Previously this script also generated the C lookup table.
|
||||
# That path has been replaced by the composable
|
||||
# error-code registration system (link-time arrays + err_codes_extract.py).
|
||||
# The RST generation is kept so the esp_docs Sphinx extension
|
||||
# (esp_docs.idf_extensions.esp_err_definitions) continues to work
|
||||
# unchanged.
|
||||
|
||||
import argparse
|
||||
import builtins
|
||||
import collections
|
||||
import fnmatch
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import textwrap
|
||||
from typing import Any
|
||||
import sys
|
||||
from typing import TextIO
|
||||
|
||||
# ignore files and dirs: Please use tools/ci/esp_err_to_name_exceptions.txt to add exceptions
|
||||
ignore_files: list = []
|
||||
ignore_dirs: list = []
|
||||
|
||||
# macros from here have higher priorities in case of collisions
|
||||
priority_headers = [os.path.join('components', 'esp_common', 'include', 'esp_err.h')]
|
||||
|
||||
# The following headers won't be included. This is useful if they are permanently included from esp_err_to_name.c.in.
|
||||
dont_include = [os.path.join('soc', 'soc.h'), os.path.join('esp_err.h')]
|
||||
|
||||
# Don't search the following directories, e.g. test directories
|
||||
exclude_search_dirs = ['test_apps', 'unit-test-app']
|
||||
|
||||
err_dict = collections.defaultdict(list) # identified errors are stored here; mapped by the error code
|
||||
rev_err_dict = dict() # map of error string to error code
|
||||
unproc_list = list() # errors with unknown codes which depend on other errors
|
||||
# Allow importing err_codes_extract from the same directory
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from err_codes_extract import extract_all # noqa: E402
|
||||
from err_codes_extract import search_headers # noqa: E402
|
||||
|
||||
|
||||
class ErrItem:
|
||||
def generate_rst_output(idf_path: str, fout: TextIO) -> None:
|
||||
"""
|
||||
Contains information about the error:
|
||||
- name - error string
|
||||
- file - relative path inside the IDF project to the file which defines this error
|
||||
- include_as - (optional) overwrites the include determined from file
|
||||
- comment - (optional) comment for the error
|
||||
- rel_str - (optional) error string which is a base for the error
|
||||
- rel_off - (optional) offset in relation to the base error
|
||||
Generate RST output using the composable error code extraction system
|
||||
(err_codes_extract.py). Produces the format expected by the
|
||||
``.. include-build-file:: inc/esp_err_defs.inc`` directive.
|
||||
"""
|
||||
components_dir = os.path.join(idf_path, 'components')
|
||||
headers = search_headers([components_dir], idf_path)
|
||||
err_codes = extract_all(headers, idf_path)
|
||||
entries = [(ec.name, ec.value, ec.comment) for ec in err_codes if ec.value is not None]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
file: str,
|
||||
include_as: Any | None = None,
|
||||
comment: str = '',
|
||||
rel_str: str = '',
|
||||
rel_off: int = 0,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.file = file
|
||||
self.include_as = include_as
|
||||
self.comment = comment
|
||||
self.rel_str = rel_str
|
||||
self.rel_off = rel_off
|
||||
# Sort: negative values first (ascending), then non-negative (ascending)
|
||||
entries.sort(key=lambda e: (e[1], e[0]))
|
||||
|
||||
def __str__(self) -> str:
|
||||
ret = self.name + ' from ' + self.file
|
||||
if self.rel_str != '':
|
||||
ret += ' is (' + self.rel_str + ' + ' + str(self.rel_off) + ')'
|
||||
if self.comment != '':
|
||||
ret += ' // ' + self.comment
|
||||
return ret
|
||||
|
||||
def __cmp__(self, other) -> int:
|
||||
if self.file in priority_headers and other.file not in priority_headers:
|
||||
return -1
|
||||
elif self.file not in priority_headers and other.file in priority_headers:
|
||||
return 1
|
||||
|
||||
base = '_BASE'
|
||||
|
||||
if self.file == other.file:
|
||||
if self.name.endswith(base) and not other.name.endswith(base):
|
||||
return 1
|
||||
elif not self.name.endswith(base) and other.name.endswith(base):
|
||||
return -1
|
||||
|
||||
self_key = self.file + self.name
|
||||
other_key = other.file + other.name
|
||||
if self_key < other_key:
|
||||
return -1
|
||||
elif self_key > other_key:
|
||||
return 1
|
||||
for name, value, comment in entries:
|
||||
fout.write(f':c:macro:`{name}` ')
|
||||
if value > 0:
|
||||
fout.write(f'**(0x{value:x})**')
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
class InputError(RuntimeError):
|
||||
"""
|
||||
Represents and error on the input
|
||||
"""
|
||||
|
||||
def __init__(self, p: str, e: str) -> None:
|
||||
super().__init__(f'{p}: {e}')
|
||||
|
||||
|
||||
def process(line: str, idf_path: str, include_as: Any) -> None:
|
||||
"""
|
||||
Process a line of text from file idf_path (relative to IDF project).
|
||||
Fills the global list unproc_list and dictionaries err_dict, rev_err_dict
|
||||
"""
|
||||
if idf_path.endswith('.c'):
|
||||
# We would not try to include a C file
|
||||
raise InputError(idf_path, f'This line should be in a header file: {line}')
|
||||
|
||||
words = re.split(r' +', line, 2)
|
||||
# words[1] is the error name
|
||||
# words[2] is the rest of the line (value, base + value, comment)
|
||||
if len(words) < 3:
|
||||
raise InputError(idf_path, f'Error at line {line}')
|
||||
|
||||
line = ''
|
||||
todo_str = words[2]
|
||||
|
||||
comment = ''
|
||||
# identify possible comment
|
||||
m = re.search(r'/\*!<(.+?(?=\*/))', todo_str)
|
||||
if m:
|
||||
comment = m.group(1).strip()
|
||||
todo_str = todo_str[: m.start()].strip() # keep just the part before the comment
|
||||
|
||||
# identify possible parentheses ()
|
||||
m = re.search(r'\((.+)\)', todo_str)
|
||||
if m:
|
||||
todo_str = m.group(1) # keep what is inside the parentheses
|
||||
|
||||
# identify BASE error code, e.g. from the form BASE + 0x01
|
||||
m = re.search(r'\s*(\w+)\s*\+(.+)', todo_str)
|
||||
if m:
|
||||
related = m.group(1) # BASE
|
||||
todo_str = m.group(2) # keep and process only what is after "BASE +"
|
||||
|
||||
# try to match a hexadecimal number
|
||||
m = re.search(r'0x([0-9A-Fa-f]+)', todo_str)
|
||||
if m:
|
||||
num = int(m.group(1), 16)
|
||||
else:
|
||||
# Try to match a decimal number. Negative value is possible for some numbers, e.g. ESP_FAIL
|
||||
m = re.search(r'(-?[0-9]+)', todo_str)
|
||||
if m:
|
||||
num = int(m.group(1), 10)
|
||||
elif re.match(r'\w+', todo_str):
|
||||
# It is possible that there is no number, e.g. #define ERROR BASE
|
||||
related = todo_str # BASE error
|
||||
num = 0 # (BASE + 0)
|
||||
else:
|
||||
raise InputError(idf_path, f'Cannot parse line {line}')
|
||||
|
||||
try:
|
||||
related
|
||||
except NameError:
|
||||
# The value of the error is known at this moment because it do not depends on some other BASE error code
|
||||
err_dict[num].append(ErrItem(words[1], idf_path, include_as, comment))
|
||||
rev_err_dict[words[1]] = num
|
||||
else:
|
||||
# Store the information available now and compute the error code later
|
||||
unproc_list.append(ErrItem(words[1], idf_path, include_as, comment, related, num))
|
||||
|
||||
|
||||
def process_remaining_errors() -> None:
|
||||
"""
|
||||
Create errors which could not be processed before because the error code
|
||||
for the BASE error code wasn't known.
|
||||
This works for sure only if there is no multiple-time dependency, e.g.:
|
||||
#define BASE1 0
|
||||
#define BASE2 (BASE1 + 10)
|
||||
#define ERROR (BASE2 + 10) - ERROR will be processed successfully only if it processed later than BASE2
|
||||
"""
|
||||
for item in unproc_list:
|
||||
if item.rel_str in rev_err_dict:
|
||||
base_num = rev_err_dict[item.rel_str]
|
||||
num = base_num + item.rel_off
|
||||
err_dict[num].append(ErrItem(item.name, item.file, item.include_as, item.comment))
|
||||
rev_err_dict[item.name] = num
|
||||
else:
|
||||
print(item.rel_str + ' referenced by ' + item.name + ' in ' + item.file + ' is unknown')
|
||||
|
||||
del unproc_list[:]
|
||||
|
||||
|
||||
def path_to_include(path: str) -> str:
|
||||
"""
|
||||
Process the path (relative to the IDF project) in a form which can be used
|
||||
to include in a C file. Using just the filename does not work all the
|
||||
time because some files are deeper in the tree. This approach tries to
|
||||
find an 'include' parent directory an include its subdirectories, e.g.
|
||||
"components/XY/include/esp32/file.h" will be transported into "esp32/file.h"
|
||||
So this solution works only works when the subdirectory or subdirectories
|
||||
are inside the "include" directory. Other special cases need to be handled
|
||||
here when the compiler gives an unknown header file error message.
|
||||
"""
|
||||
spl_path = path.split(os.sep)
|
||||
try:
|
||||
i = spl_path.index('include')
|
||||
except ValueError:
|
||||
# no include in the path -> use just the filename
|
||||
return os.path.basename(path)
|
||||
else:
|
||||
return os.sep.join(spl_path[i + 1 :]) # subdirectories and filename in "include"
|
||||
|
||||
|
||||
def print_warning(error_list: list, error_code: int) -> None:
|
||||
"""
|
||||
Print warning about errors with the same error code
|
||||
"""
|
||||
print(f'[WARNING] The following errors have the same code ({error_code}):')
|
||||
for e in error_list:
|
||||
print(f' {e}')
|
||||
|
||||
|
||||
def max_string_width() -> int:
|
||||
max_width = 0
|
||||
for k in err_dict:
|
||||
for e in err_dict[k]:
|
||||
x = len(e.name)
|
||||
if x > max_width:
|
||||
max_width = x
|
||||
return max_width
|
||||
|
||||
|
||||
def generate_c_output(fin: TextIO, fout: TextIO) -> None:
|
||||
"""
|
||||
Writes the output to fout based on th error dictionary err_dict and
|
||||
template file fin.
|
||||
"""
|
||||
# make includes unique by using a set
|
||||
includes = set()
|
||||
for k in err_dict:
|
||||
for e in err_dict[k]:
|
||||
if e.include_as:
|
||||
includes.add(e.include_as)
|
||||
else:
|
||||
includes.add(path_to_include(e.file))
|
||||
|
||||
# The order in a set in non-deterministic therefore it could happen that the
|
||||
# include order will be different in other machines and false difference
|
||||
# in the output file could be reported. In order to avoid this, the items
|
||||
# are sorted in a list.
|
||||
include_list = list(includes)
|
||||
include_list.sort()
|
||||
|
||||
max_width = max_string_width() + 17 + 1 # length of " ERR_TBL_IT()," with spaces is 17
|
||||
max_decdig = max(len(str(k)) for k in err_dict)
|
||||
|
||||
for line in fin:
|
||||
if re.match(r'@COMMENT@', line):
|
||||
fout.write('//Do not edit this file because it is autogenerated by ' + os.path.basename(__file__) + '\n')
|
||||
|
||||
elif re.match(r'@HEADERS@', line):
|
||||
for i in include_list:
|
||||
if i not in dont_include:
|
||||
fout.write('#if __has_include("' + i + '")\n#include "' + i + '"\n#endif\n')
|
||||
elif re.match(r'@ERROR_ITEMS@', line):
|
||||
last_file = ''
|
||||
for k in sorted(err_dict.keys()):
|
||||
if len(err_dict[k]) > 1:
|
||||
err_dict[k].sort(key=functools.cmp_to_key(ErrItem.__cmp__))
|
||||
print_warning(err_dict[k], k)
|
||||
for e in err_dict[k]:
|
||||
if e.file != last_file:
|
||||
last_file = e.file
|
||||
fout.write(f' // {last_file}\n')
|
||||
table_line = (
|
||||
(' ERR_TBL_IT(' + e.name + '), ').ljust(max_width) + '/* ' + str(k).rjust(max_decdig)
|
||||
)
|
||||
fout.write(f'# ifdef {e.name}\n')
|
||||
fout.write(table_line)
|
||||
hexnum_length = 0
|
||||
if k > 0: # negative number and zero should be only ESP_FAIL and ESP_OK
|
||||
hexnum = f' 0x{k:x}'
|
||||
hexnum_length = len(hexnum)
|
||||
fout.write(hexnum)
|
||||
if e.comment != '':
|
||||
if len(e.comment) < 50:
|
||||
fout.write(f' {e.comment}')
|
||||
else:
|
||||
indent = ' ' * (len(table_line) + hexnum_length + 1)
|
||||
w = textwrap.wrap(e.comment, width=120, initial_indent=indent, subsequent_indent=indent)
|
||||
# this couldn't be done with initial_indent because there is no initial_width option
|
||||
fout.write(f' {w[0].strip()}')
|
||||
for i in range(1, len(w)):
|
||||
fout.write(f'\n{w[i]}')
|
||||
fout.write(' */\n# endif\n')
|
||||
else:
|
||||
fout.write(line)
|
||||
|
||||
|
||||
def generate_rst_output(fout: TextIO) -> None:
|
||||
for k in sorted(err_dict.keys()):
|
||||
v = err_dict[k][0]
|
||||
fout.write(f':c:macro:`{v.name}` ')
|
||||
if k > 0:
|
||||
fout.write(f'**(0x{k:x})**')
|
||||
else:
|
||||
fout.write(f'({k:d})')
|
||||
if len(v.comment) > 0:
|
||||
fout.write(f': {v.comment}')
|
||||
fout.write(f'({value:d})')
|
||||
if comment:
|
||||
fout.write(f': {comment}')
|
||||
fout.write('\n\n')
|
||||
|
||||
|
||||
@@ -310,72 +54,12 @@ def main() -> None:
|
||||
else:
|
||||
idf_path = os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
||||
|
||||
parser = argparse.ArgumentParser(description='ESP32 esp_err_to_name lookup generator for esp_err_t')
|
||||
parser.add_argument(
|
||||
'--c_input',
|
||||
help='Path to the esp_err_to_name.c.in template input.',
|
||||
default=idf_path + '/components/esp_common/src/esp_err_to_name.c.in',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--c_output',
|
||||
help='Path to the esp_err_to_name.c output.',
|
||||
default=idf_path + '/components/esp_common/src/esp_err_to_name.c',
|
||||
)
|
||||
parser.add_argument('--rst_output', help='Generate .rst output and save it into this file')
|
||||
parser.add_argument('--exceptions_file', default=idf_path + '/tools/ci/esp_err_to_name_exceptions.txt')
|
||||
parser = argparse.ArgumentParser(description='ESP-IDF error code RST documentation generator')
|
||||
parser.add_argument('--rst_output', required=True, help='Generate .rst output and save it into this file')
|
||||
args = parser.parse_args()
|
||||
|
||||
include_as_pattern = re.compile(rf'\s*//\s*{os.path.basename(__file__)}: [^"]* "([^"]+)"')
|
||||
define_pattern = re.compile(r'\s*#define\s+(ESP_ERR_|ESP_OK|ESP_FAIL)')
|
||||
|
||||
if args.exceptions_file and os.path.exists(args.exceptions_file):
|
||||
with builtins.open(args.exceptions_file, encoding='utf-8') as exceptions_file:
|
||||
for line in exceptions_file:
|
||||
entry = line.strip()
|
||||
if not entry or entry.startswith('#'):
|
||||
continue
|
||||
if entry.endswith('/'):
|
||||
ignore_dirs.append(entry.rstrip('/'))
|
||||
else:
|
||||
ignore_files.append(entry)
|
||||
|
||||
for root, dirnames, filenames in os.walk(os.path.join(idf_path, 'components'), topdown=True):
|
||||
# When topdown is True, we can modify the dirnames list in-place
|
||||
# walk() will only recurse into the subdirectories whose names remain in dirnames
|
||||
dirnames[:] = [d for d in dirnames if d not in exclude_search_dirs]
|
||||
for filename in fnmatch.filter(filenames, '*.[ch]'):
|
||||
full_path = os.path.join(root, filename)
|
||||
path_in_idf = os.path.relpath(full_path, idf_path)
|
||||
if path_in_idf in ignore_files or any(path_in_idf.startswith(d) for d in ignore_dirs):
|
||||
continue
|
||||
with builtins.open(full_path, encoding='utf-8') as f:
|
||||
try:
|
||||
include_as = None
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
m = include_as_pattern.search(line)
|
||||
if m:
|
||||
include_as = m.group(1)
|
||||
# match also ESP_OK and ESP_FAIL because some of ESP_ERRs are referencing them
|
||||
elif define_pattern.match(line):
|
||||
try:
|
||||
process(line, path_in_idf, include_as)
|
||||
except InputError as e:
|
||||
print(e)
|
||||
except UnicodeDecodeError:
|
||||
raise ValueError(f'The encoding of {path_in_idf} is not Unicode.')
|
||||
|
||||
process_remaining_errors()
|
||||
|
||||
if args.rst_output is not None:
|
||||
with builtins.open(args.rst_output, 'w', encoding='utf-8') as fout:
|
||||
generate_rst_output(fout)
|
||||
else:
|
||||
with (
|
||||
builtins.open(args.c_input, encoding='utf-8') as fin,
|
||||
builtins.open(args.c_output, 'w', encoding='utf-8') as fout,
|
||||
):
|
||||
generate_c_output(fin, fout)
|
||||
with open(args.rst_output, 'w', encoding='utf-8') as fout:
|
||||
generate_rst_output(idf_path, fout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -16,6 +16,10 @@ tools/test_apps/build_system/embed_test:
|
||||
temporary: false
|
||||
reason: Hardware independent feature, no need to test on all targets
|
||||
|
||||
tools/test_apps/build_system/err_codes_check:
|
||||
enable:
|
||||
- if: IDF_TARGET in ["esp32", "linux"]
|
||||
|
||||
tools/test_apps/build_system/external_project:
|
||||
enable:
|
||||
- if: IDF_TARGET in ["esp32", "esp32c3"]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# The following 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)
|
||||
|
||||
# On Linux/macOS, limit to components that support the host target.
|
||||
# The generated test uses #ifdef guards, so error codes from excluded
|
||||
# components are simply skipped.
|
||||
if(IDF_TARGET STREQUAL "linux")
|
||||
set(COMPONENTS main)
|
||||
endif()
|
||||
|
||||
project(err_codes_check)
|
||||
@@ -0,0 +1,25 @@
|
||||
| Supported Targets | ESP32 | Linux |
|
||||
| ----------------- | ----- | ----- |
|
||||
|
||||
# Error Codes Registration Check
|
||||
|
||||
This test app verifies that all `ESP_ERR_*` error codes defined across ESP-IDF
|
||||
components are correctly registered via `idf_define_esp_err_codes()` and can be
|
||||
resolved by `esp_err_to_name()`.
|
||||
|
||||
## How it works
|
||||
|
||||
1. At CMake configure time, `err_codes_extract.py` scans all ESP-IDF component
|
||||
headers to collect every `ESP_ERR_*` define.
|
||||
2. A test source file is generated that calls `esp_err_to_name()` for each
|
||||
collected error code and asserts the result matches the expected name.
|
||||
3. The app is built and run (e.g., in QEMU or on hardware).
|
||||
|
||||
## Building and running
|
||||
|
||||
```bash
|
||||
cd tools/test_apps/build_system/err_codes_check
|
||||
idf.py set-target esp32
|
||||
idf.py build
|
||||
# Run in QEMU or flash to hardware
|
||||
```
|
||||
@@ -0,0 +1,78 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Generate a C test source file that validates all ESP-IDF error codes
|
||||
# are correctly registered and resolvable via esp_err_to_name().
|
||||
#
|
||||
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def generate_test_source(csv_path: str, output_path: str) -> None:
|
||||
"""Generate a C source file that tests all error codes."""
|
||||
entries = []
|
||||
with open(csv_path, encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
name = row['name']
|
||||
value = int(row['value'])
|
||||
entries.append((name, value))
|
||||
|
||||
lines = []
|
||||
lines.append('/*')
|
||||
lines.append(' * Auto-generated by gen_err_codes_test.py — do not edit')
|
||||
lines.append(' */')
|
||||
lines.append('')
|
||||
lines.append('#include <stdio.h>')
|
||||
lines.append('#include <string.h>')
|
||||
lines.append('#include "esp_err.h"')
|
||||
lines.append('')
|
||||
lines.append('/* Forward declaration */')
|
||||
lines.append('int check_err_code(esp_err_t code, const char *expected_name);')
|
||||
lines.append('')
|
||||
lines.append('int check_err_code(esp_err_t code, const char *expected_name)')
|
||||
lines.append('{')
|
||||
lines.append(' const char *actual = esp_err_to_name(code);')
|
||||
lines.append(' if (strcmp(actual, expected_name) != 0) {')
|
||||
lines.append(' printf("FAIL: esp_err_to_name(0x%x) = \\"%s\\", expected \\"%s\\"\\n",')
|
||||
lines.append(' (unsigned)code, actual, expected_name);')
|
||||
lines.append(' return 1;')
|
||||
lines.append(' }')
|
||||
lines.append(' return 0;')
|
||||
lines.append('}')
|
||||
lines.append('')
|
||||
lines.append('int err_codes_check_all(void)')
|
||||
lines.append('{')
|
||||
lines.append(' int failures = 0;')
|
||||
|
||||
for name, value in entries:
|
||||
# Use #ifdef guards as the error code may not be defined for all targets
|
||||
lines.append(f'#ifdef {name}')
|
||||
lines.append(f' failures += check_err_code({name}, "{name}");')
|
||||
lines.append('#endif')
|
||||
|
||||
lines.append('')
|
||||
lines.append(' if (failures == 0) {')
|
||||
lines.append(f' printf("All {len(entries)} error code checks passed.\\n");')
|
||||
lines.append(' } else {')
|
||||
lines.append(f' printf("%d of {len(entries)} error code checks failed.\\n", failures);')
|
||||
lines.append(' }')
|
||||
lines.append(' return failures;')
|
||||
lines.append('}')
|
||||
lines.append('')
|
||||
|
||||
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines))
|
||||
|
||||
print(f'Generated {output_path} with {len(entries)} error code checks')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) != 3:
|
||||
print(f'Usage: {sys.argv[0]} <input.csv> <output.c>', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
generate_test_source(sys.argv[1], sys.argv[2])
|
||||
@@ -0,0 +1,33 @@
|
||||
set(idf_path "$ENV{IDF_PATH}")
|
||||
set(project_dir "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||
|
||||
# Step 1: Extract all error codes from the entire ESP-IDF components directory
|
||||
set(all_err_csv "${CMAKE_CURRENT_BINARY_DIR}/all_esp_err_codes.csv")
|
||||
execute_process(
|
||||
COMMAND ${PYTHON} "${idf_path}/tools/err_codes_extract.py"
|
||||
--search-dirs "components"
|
||||
--base-path "${idf_path}"
|
||||
--output "${all_err_csv}"
|
||||
RESULT_VARIABLE extract_result
|
||||
OUTPUT_VARIABLE extract_output
|
||||
ERROR_VARIABLE extract_error
|
||||
)
|
||||
if(NOT extract_result EQUAL 0)
|
||||
message(FATAL_ERROR "err_codes_extract.py failed: ${extract_error}")
|
||||
endif()
|
||||
|
||||
# Step 2: Generate the test source file from the CSV
|
||||
set(test_src "${CMAKE_CURRENT_BINARY_DIR}/err_codes_test_cases.c")
|
||||
execute_process(
|
||||
COMMAND ${PYTHON} "${project_dir}/gen_err_codes_test.py"
|
||||
"${all_err_csv}" "${test_src}"
|
||||
RESULT_VARIABLE gen_result
|
||||
OUTPUT_VARIABLE gen_output
|
||||
ERROR_VARIABLE gen_error
|
||||
)
|
||||
if(NOT gen_result EQUAL 0)
|
||||
message(FATAL_ERROR "gen_err_codes_test.py failed: ${gen_error}")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS "test_main.c" "${test_src}"
|
||||
INCLUDE_DIRS ".")
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
/* Defined in the generated err_codes_test_cases.c */
|
||||
extern int err_codes_check_all(void);
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
printf("Starting error codes registration check...\n");
|
||||
|
||||
int failures = err_codes_check_all();
|
||||
|
||||
if (failures == 0) {
|
||||
printf("SUCCESS: All error codes are correctly registered.\n");
|
||||
} else {
|
||||
printf("FAILURE: %d error codes are not correctly registered.\n", failures);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import pytest
|
||||
from pytest_embedded_idf.dut import IdfDut
|
||||
from pytest_embedded_idf.utils import idf_parametrize
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@idf_parametrize('target', ['esp32'], indirect=['target'])
|
||||
def test_err_codes_check(dut: IdfDut) -> None:
|
||||
dut.expect_exact('SUCCESS: All error codes are correctly registered.', timeout=120)
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@idf_parametrize('target', ['linux'], indirect=['target'])
|
||||
def test_err_codes_check_linux(dut: IdfDut) -> None:
|
||||
dut.expect_exact('SUCCESS: All error codes are correctly registered.', timeout=120)
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.macos
|
||||
@idf_parametrize('target', ['linux'], indirect=['target'])
|
||||
def test_err_codes_check_macos(dut: IdfDut) -> None:
|
||||
dut.expect_exact('SUCCESS: All error codes are correctly registered.', timeout=120)
|
||||
Reference in New Issue
Block a user