mirror of
https://github.com/eclipse-threadx/threadx.git
synced 2026-10-06 06:59:08 +08:00
Added a Module Manager host test harness and released a module thread's kernel stack only when it has one (#738)
* Released a module thread's kernel stack only when it has one, so deleting a thread without a manager-allocated stack no longer asks for that memory back The thread delete dispatcher decided whether to release a kernel stack from the calling module's property flags. A user-mode module can be given the address of a thread that carries no kernel stack the manager allocated, and deleting it passed that thread's null stack pointer to _txm_module_manager_object_deallocate(). The pointer is now tested instead of the module's properties. Both thread create paths clear the whole control block before filling it in, so a thread with no manager-allocated kernel stack holds TX_NULL in the field, which makes the test exact rather than an inference from how the module was loaded. Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com> * Added a Module Manager host test harness and covered the user-mode thread kernel stack lifetime The Module Manager is not in the ThreadX library the test tree links, and its control blocks come from a module port rather than a base port, so nothing in the suite could reach it. The thread_transition directory already describes this technique as the one "the module manager tests in this tree use", but no such tests existed. This adds the directory that comment refers to: a port shim that replaces the three interrupt primitives with host equivalents that count, and a CMake target that compiles the manager sources under test directly against one module port's headers. The dispatch layer is one header of static functions and an unoptimised build emits all of them, so a test that includes it to reach one dispatcher would pull in references to every service the manager can dispatch. The header guards each dispatcher with its own TXM_<SERVICE>_CALL_NOT_USED macro, so the build reads that list out of the header and defines every guard except the ones the test needs. Reading it rather than writing it down means a service added later is excluded without anyone having to remember. The first test asserts the invariant a user-mode module thread has to hold: one logical thread costs the object pool two allocations, the control block the module asks for and the kernel stack the manager takes on its behalf, and deleting the thread must return the pool's available bytes and the module's allocation-list count to exactly what they were. It asserts an equality rather than a bound, because a bound would pass while one of the two was left behind on every cycle, and then runs enough cycles that a leak of one stack per cycle exhausts the pool several times over. Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
This commit is contained in:
@@ -2078,9 +2078,14 @@ ALIGN_TYPE stack_status;
|
||||
|
||||
return_value = (ALIGN_TYPE) _txe_thread_delete(thread_ptr);
|
||||
|
||||
/* Deallocate the kernel stack for a user-mode thread. */
|
||||
/* Deallocate the kernel stack for a user-mode thread. The pointer is tested
|
||||
rather than the module's properties: both thread create paths clear the whole
|
||||
control block before filling it in, so a thread that carries no kernel stack
|
||||
the manager allocated holds TX_NULL here, and a user-mode module can be given
|
||||
the address of such a thread. */
|
||||
if ((return_value == TX_SUCCESS) &&
|
||||
(module_instance -> txm_module_instance_property_flags & TXM_MODULE_USER_MODE))
|
||||
(module_instance -> txm_module_instance_property_flags & TXM_MODULE_USER_MODE) &&
|
||||
(thread_ptr -> tx_thread_module_kernel_stack_start != TX_NULL))
|
||||
{
|
||||
stack_status = _txm_module_manager_object_deallocate(thread_ptr -> tx_thread_module_kernel_stack_start);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ add_subdirectory(samples)
|
||||
# there turns a set of clean merges into a set of one-line conflicts.
|
||||
add_subdirectory(thread_transition)
|
||||
|
||||
# Host tests for the Module Manager. Listed after thread_transition for the reason
|
||||
# given above it: additions here are appended rather than inserted, so that branches
|
||||
# adding a directory of their own merge cleanly.
|
||||
add_subdirectory(module_manager)
|
||||
|
||||
# Coverage
|
||||
#
|
||||
# The gate here used to be the build type alone, and only one of the five
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
|
||||
cmake_policy(SET CMP0057 NEW)
|
||||
|
||||
project(module_manager_test LANGUAGES C)
|
||||
|
||||
set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../..)
|
||||
set(SOURCE_DIR ${REPO_ROOT}/test/tx/module_manager)
|
||||
|
||||
# The Module Manager is not part of the ThreadX library this test tree builds, and
|
||||
# its control blocks come from a module port rather than a base port. These tests
|
||||
# therefore compile the manager sources they exercise directly against one module
|
||||
# port's headers, and stand stubs behind the kernel services those sources call, so
|
||||
# that they run on the host like every other test here. It is the same technique the
|
||||
# thread_transition directory uses, for the same reason: the code under test cannot
|
||||
# be reached through the library the suite links.
|
||||
#
|
||||
# Every module port is an embedded architecture, so the interrupt primitives those
|
||||
# sources use are inline assembly the host assembler cannot take. The port shim
|
||||
# header is force-included ahead of every translation unit here and replaces the
|
||||
# three of them with host equivalents that count.
|
||||
|
||||
set(module_manager_dir ${REPO_ROOT}/common_modules/module_manager)
|
||||
set(cortex_a7_module_dir ${REPO_ROOT}/ports_module/cortex_a7/gnu)
|
||||
|
||||
add_executable(
|
||||
threadx_module_manager_thread_kernel_stack_test
|
||||
${SOURCE_DIR}/threadx_module_manager_thread_kernel_stack_test.c
|
||||
${module_manager_dir}/src/txm_module_manager_util.c
|
||||
${module_manager_dir}/src/txm_module_manager_object_allocate.c
|
||||
${module_manager_dir}/src/txm_module_manager_object_deallocate.c)
|
||||
|
||||
target_include_directories(
|
||||
threadx_module_manager_thread_kernel_stack_test
|
||||
PRIVATE ${SOURCE_DIR}
|
||||
${REPO_ROOT}/common/inc
|
||||
${REPO_ROOT}/common_modules/inc
|
||||
${module_manager_dir}/inc
|
||||
${cortex_a7_module_dir}/inc)
|
||||
|
||||
# The dispatch layer is one header of static functions, one per kernel service. A
|
||||
# test reaches the dispatcher it exercises by including that header, which defines
|
||||
# every other dispatcher too, and an unoptimised build emits them all -- so their
|
||||
# references to services this harness stands behind nothing for would reach the
|
||||
# linker.
|
||||
#
|
||||
# The header guards each dispatcher with its own TXM_<SERVICE>_CALL_NOT_USED macro,
|
||||
# which is the supported way to trim the dispatch table. Defining every guard except
|
||||
# the ones a test needs leaves that test's translation unit holding only the
|
||||
# dispatcher under test. The list is read out of the header rather than written down
|
||||
# here, so a service added later is excluded without anyone having to remember.
|
||||
file(STRINGS ${module_manager_dir}/inc/txm_module_manager_dispatch.h
|
||||
dispatch_guard_lines REGEX "^#ifndef TXM_[A-Z0-9_]+_CALL_NOT_USED")
|
||||
|
||||
set(dispatch_guards "")
|
||||
foreach(guard_line ${dispatch_guard_lines})
|
||||
string(REGEX MATCH "TXM_[A-Z0-9_]+_CALL_NOT_USED" guard "${guard_line}")
|
||||
list(APPEND dispatch_guards ${guard})
|
||||
endforeach()
|
||||
list(REMOVE_DUPLICATES dispatch_guards)
|
||||
|
||||
# The dispatchers this test calls, which therefore must stay in.
|
||||
set(kernel_stack_dispatchers TXM_THREAD_DELETE_CALL_NOT_USED)
|
||||
|
||||
set(kernel_stack_guards ${dispatch_guards})
|
||||
list(REMOVE_ITEM kernel_stack_guards ${kernel_stack_dispatchers})
|
||||
|
||||
target_compile_definitions(threadx_module_manager_thread_kernel_stack_test
|
||||
PRIVATE ${kernel_stack_guards})
|
||||
|
||||
# The port shim is force-included ahead of every translation unit, the test included,
|
||||
# because the manager sources and the dispatch header both reach for the port's
|
||||
# interrupt primitives.
|
||||
target_compile_options(
|
||||
threadx_module_manager_thread_kernel_stack_test
|
||||
PRIVATE -Wall
|
||||
-Wextra
|
||||
-include ${SOURCE_DIR}/threadx_module_manager_host_test_port.h)
|
||||
|
||||
add_test(${CMAKE_BUILD_TYPE}::threadx_module_manager_thread_kernel_stack_test
|
||||
threadx_module_manager_thread_kernel_stack_test)
|
||||
@@ -0,0 +1,80 @@
|
||||
/***************************************************************************
|
||||
* 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.
|
||||
*
|
||||
* AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5).
|
||||
* The AI-generated portions may be considered public domain (CC0-1.0)
|
||||
* and not subject to the project's licence. The human contributor has
|
||||
* reviewed and verified that the code is correct.
|
||||
*
|
||||
* SPDX-License-Identifier: MIT and CC0-1.0
|
||||
**************************************************************************/
|
||||
|
||||
|
||||
/**************************************************************************/
|
||||
/**************************************************************************/
|
||||
/** */
|
||||
/** ThreadX Test */
|
||||
/** */
|
||||
/** Module Manager host test port shim */
|
||||
/** */
|
||||
/**************************************************************************/
|
||||
/**************************************************************************/
|
||||
|
||||
/* The Module Manager is not in the ThreadX library this test tree builds, and its
|
||||
control blocks come from a module port rather than a base port, so the manager
|
||||
sources under test are compiled directly against one module port's headers. Every
|
||||
module port is an embedded architecture, and their interrupt primitives are
|
||||
inline assembly for that architecture, which the host assembler cannot take.
|
||||
|
||||
This header is force-included ahead of each manager source under test. It brings
|
||||
the port's headers in first, so every declaration the source needs is the port's
|
||||
own, and then replaces the three interrupt primitives with host equivalents that
|
||||
count instead of executing. Counting is not only what makes them portable: a test
|
||||
can assert that a source under test left the interrupt lock balanced, and that a
|
||||
section which has to run with interrupts disabled did so. */
|
||||
|
||||
#ifndef THREADX_MODULE_MANAGER_HOST_TEST_PORT_H
|
||||
#define THREADX_MODULE_MANAGER_HOST_TEST_PORT_H
|
||||
|
||||
#define TX_SOURCE_CODE
|
||||
|
||||
#include "tx_api.h"
|
||||
|
||||
#undef TX_INTERRUPT_SAVE_AREA
|
||||
#undef TX_DISABLE
|
||||
#undef TX_RESTORE
|
||||
|
||||
/* Nesting depth of the stand-in interrupt lock, and the deepest it has ever been.
|
||||
Defined by the test, which is the only translation unit that reads them. */
|
||||
extern unsigned int test_interrupt_disable_depth;
|
||||
extern unsigned int test_interrupt_disable_max_depth;
|
||||
extern unsigned int test_interrupt_restore_underflows;
|
||||
|
||||
#define TX_INTERRUPT_SAVE_AREA
|
||||
|
||||
#define TX_DISABLE \
|
||||
{ \
|
||||
test_interrupt_disable_depth++; \
|
||||
if (test_interrupt_disable_depth > test_interrupt_disable_max_depth) \
|
||||
{ \
|
||||
test_interrupt_disable_max_depth = test_interrupt_disable_depth; \
|
||||
} \
|
||||
}
|
||||
|
||||
#define TX_RESTORE \
|
||||
{ \
|
||||
if (test_interrupt_disable_depth == 0U) \
|
||||
{ \
|
||||
test_interrupt_restore_underflows++; \
|
||||
} \
|
||||
else \
|
||||
{ \
|
||||
test_interrupt_disable_depth--; \
|
||||
} \
|
||||
}
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user