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:
Frédéric Desbiens
2026-09-14 16:08:48 -04:00
committed by GitHub
parent dde43b8ab2
commit 83a3e62c28
5 changed files with 707 additions and 2 deletions
@@ -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);
}
+5
View File
@@ -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