From 120c06ebe11b800693340c261152580d0d3f31aa Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 17:33:04 +0100 Subject: [PATCH 01/31] refactor: S11.01 pool CircularBuffer instance struct, keep ring caller-supplied MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the previously-conflated CircularBuffer storage. The instance bookkeeping struct (vtable, mutex pointer, ring pointer, head/tail/wrap) now lives in a library-internal static pool; the caller continues to supply the ring memory as plain uint8_t* + size. This addresses the original E11 driver — eliminating the void* storage cast on the instance — without conflating it with a tunable for ring capacity (which would not work: different call sites need different ring sizes inside the same build). Public API change: - SolidSyslogCircularBuffer_Create(mutex, ring, ringBytes) replaces Create(storage, storageBytes, mutex). - SolidSyslogCircularBufferStorage typedef, OVERHEAD constant, and STORAGE_SIZE / STORAGE_SIZE_BYTES macros removed. - New convenience macro SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(maxMessages) for sizing the caller's ring. - HEADER_BYTES stays public — needed by the new macro. Three-TU split (E11 canonical pattern): - SolidSyslogCircularBuffer.c: vtable + ring logic. Exposes CircularBuffer_Initialise(self, mutex, ring, ringBytes) and CircularBuffer_Cleanup(self) via SolidSyslogCircularBufferPrivate.h. - SolidSyslogCircularBufferPrivate.h: struct definition + helper signatures. TU-local; tests must not include it. - SolidSyslogCircularBufferStatic.c: 1-slot static pool, public Create/Destroy. Next commit grows pool size, adds the fallback object, wraps slot walks in SolidSyslog_LockConfig/UnlockConfig, and handles unknown-pointer Destroy. CMake gate (E11): - SOLIDSYSLOG_ALLOCATION_STRATEGY cache var (static|dynamic, default static). dynamic errors with FATAL_ERROR — no dynamic TUs ship yet. MISRA / static analysis: - Storage-cast suppression (CircularBuffer.c:72 SelfFromStorage) deleted — function gone. - 18.7 FAM suppression (CircularBuffer.c:26) deleted — struct no longer has a flexible array member; ring is now an external pointer. - 2.5 STORAGE_SIZE macro suppression deleted — macro gone, RING_BYTES takes the remaining slot. - 5.7 + 2.4 + 11.3 (SelfFromBase) + 2.4 + 5.7 (anonymous enums) suppression line numbers updated. - One new constParameter inline suppression in Static.c for the transitional 1-slot Destroy that only compares the base pointer; next commit's pool walk + Cleanup wiring mutates through it and the suppression goes away. - Net unsuppressed cppcheck-misra findings 93 -> 87 across the scanned Core+Platform tree. Doc updates: - CLAUDE.md public-header audience table: row rewritten for new API. - README.md public-headers list: bullet updated to call out caller supplies ring memory only, instance lives in static pool. - docs/NAMING.md: SelfFromStorage example switched to BlockStore (still on the caller-storage pattern); added a note that E11-migrated classes (currently only CircularBuffer) no longer use the cast. - docs/misra-deviations.md: D.002 caller-storage list drops CircularBuffer with a note pointing at E11; D.005 FAM scope reduces to Formatter only; D.012 macro inventory updated to RING_BYTES. - docs/iec62443.md: "caller-allocated storage" -> "caller-allocated ring memory" in the RTOS-integration prose. - SKILL.md: matching prose update. Tests + BDD targets updated: - HandleEqualsStorageAddress test removed (caller no longer addresses the instance struct; only the ring is at a known address). - TEST_MAX_MESSAGES / BDD_TARGET_BUFFER_MESSAGES / etc. constants kept unchanged at every call site — only the API mechanics changed. Gates all green locally: - debug, clang-debug, sanitize: 1118/1118 tests pass. - tidy, cppcheck, cppcheck-misra: clean. - coverage: 99.6% lines / 99.0% functions. - cmake -DSOLIDSYSLOG_ALLOCATION_STRATEGY=dynamic errors as designed. --- Bdd/Targets/FreeRtos/main.c | 5 +- Bdd/Targets/Windows/BddTargetWindows.c | 5 +- CLAUDE.md | 2 +- CMakeLists.txt | 17 +++++ Core/Interface/SolidSyslogCircularBuffer.h | 22 ++----- Core/Source/CMakeLists.txt | 1 + Core/Source/SolidSyslogCircularBuffer.c | 65 ++++++------------- .../Source/SolidSyslogCircularBufferPrivate.h | 30 +++++++++ Core/Source/SolidSyslogCircularBufferStatic.c | 30 +++++++++ README.md | 2 +- SKILL.md | 2 +- ...SolidSyslogBlockStoreDrainOrderingTest.cpp | 4 +- Tests/SolidSyslogCircularBufferTest.cpp | 17 ++--- Tests/SolidSyslogTest.cpp | 5 +- docs/NAMING.md | 24 ++++--- docs/iec62443.md | 2 +- docs/misra-deviations.md | 49 +++++++------- misra_suppressions.txt | 11 ++-- 18 files changed, 168 insertions(+), 125 deletions(-) create mode 100644 Core/Source/SolidSyslogCircularBufferPrivate.h create mode 100644 Core/Source/SolidSyslogCircularBufferStatic.c diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index 98d5466b..10aed951 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -169,8 +169,7 @@ enum BDD_TARGET_BUFFER_MESSAGES = 8 }; -static SolidSyslogCircularBufferStorage - bufferStorage[SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE(BDD_TARGET_BUFFER_MESSAGES)]; +static uint8_t bufferRing[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(BDD_TARGET_BUFFER_MESSAGES)]; static SolidSyslogFreeRtosMutexStorage mutexStorage; /* Lifecycle mutex serialises SolidSyslog_Service against the rebuild path @@ -825,7 +824,7 @@ static void InteractiveTask(void* argument) * is the Service task; its Write side is whichever task calls * SolidSyslog_Log. */ bufferMutex = SolidSyslogFreeRtosMutex_Create(&mutexStorage); - buffer = SolidSyslogCircularBuffer_Create(bufferStorage, sizeof(bufferStorage), bufferMutex); + buffer = SolidSyslogCircularBuffer_Create(bufferMutex, bufferRing, sizeof(bufferRing)); /* Lifecycle mutex created up front so the Service task can take it * from its very first iteration without a NULL check. */ diff --git a/Bdd/Targets/Windows/BddTargetWindows.c b/Bdd/Targets/Windows/BddTargetWindows.c index 5d20d068..682d1404 100644 --- a/Bdd/Targets/Windows/BddTargetWindows.c +++ b/Bdd/Targets/Windows/BddTargetWindows.c @@ -66,8 +66,7 @@ static const char* const THRESHOLD_MARKER_PATH = "Bdd/output/solidsyslog_thresho static SolidSyslogWinsockTcpStreamStorage tcpStreamStorage; static SolidSyslogStreamSenderStorage tcpSenderStorage; -static SolidSyslogCircularBufferStorage - bufferStorage[SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE(BDD_TARGET_BUFFER_MESSAGES)]; +static uint8_t bufferRing[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(BDD_TARGET_BUFFER_MESSAGES)]; static SolidSyslogWindowsMutexStorage mutexStorage; static SolidSyslogWindowsAtomicCounterStorage counterStorage; static volatile bool shutdownFlag; @@ -304,7 +303,7 @@ int BddTargetWindows_Run(int argc, char* argv[]) struct SolidSyslogStore* store = CreateStore(&options); struct SolidSyslogMutex* mutex = SolidSyslogWindowsMutex_Create(&mutexStorage); - struct SolidSyslogBuffer* buffer = SolidSyslogCircularBuffer_Create(bufferStorage, sizeof(bufferStorage), mutex); + struct SolidSyslogBuffer* buffer = SolidSyslogCircularBuffer_Create(mutex, bufferRing, sizeof(bufferRing)); struct SolidSyslogAtomicCounter* counter = SolidSyslogWindowsAtomicCounter_Create(&counterStorage); struct SolidSyslogMetaSdConfig metaConfig = { .Counter = counter, diff --git a/CLAUDE.md b/CLAUDE.md index b8fb7aff..2f922a70 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -338,7 +338,7 @@ live under `Core/Interface/`; platform-specific helpers (the `SolidSyslogPosix*` | `SolidSyslogBufferDefinition.h` | Buffer implementors (extension point) | `SolidSyslogBuffer` vtable struct | | `SolidSyslogNullBuffer.h` | System setup code (single-task, no buffering) | `SolidSyslogNullBuffer_Create`, `_Destroy` | | `SolidSyslogPosixMessageQueueBuffer.h` | System setup code using POSIX message queue buffer | `SolidSyslogPosixMessageQueueBuffer_Create`, `_Destroy` | -| `SolidSyslogCircularBuffer.h` | System setup code using an in-memory ring buffer (bare-metal / RTOS / Windows) | `SolidSyslogCircularBufferStorage`, `SOLIDSYSLOG_CIRCULARBUFFER_STORAGE_SIZE(maxMessages)` (friendly: N max-sized messages), `SOLIDSYSLOG_CIRCULARBUFFER_STORAGE_SIZE_BYTES(ringBytes)` (raw byte capacity), `SolidSyslogCircularBuffer_Create(storage, sizeof(storage), mutex)`, `_Destroy(buffer)` (uint16-length-prefixed records, drop-newest on overflow, no-split wrap, mutex-injected synchronization) | +| `SolidSyslogCircularBuffer.h` | System setup code using an in-memory ring buffer (bare-metal / RTOS / Windows) | `SOLIDSYSLOG_CIRCULAR_BUFFER_HEADER_BYTES`, `SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(maxMessages)` (friendly: N max-sized messages → ring bytes), `SolidSyslogCircularBuffer_Create(mutex, ring, ringBytes)`, `_Destroy(buffer)` (uint16-length-prefixed records, drop-newest on overflow, no-split wrap, mutex-injected synchronization). Instance struct lives in a library-internal static pool (E11); caller supplies the ring memory only. | | `SolidSyslogMutex.h` | Any code holding a mutex handle | `SolidSyslogMutex_Lock`, `_Unlock` | | `SolidSyslogMutexDefinition.h` | Mutex implementors (extension point) | `SolidSyslogMutex` vtable struct (`Lock`, `Unlock`) | | `SolidSyslogNullMutex.h` | System setup code (single-task, no synchronization) | `SolidSyslogNullMutex_Create`, `_Destroy` | diff --git a/CMakeLists.txt b/CMakeLists.txt index 86784b0d..3816a70c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,23 @@ if(SOLIDSYSLOG_OPENSSL) endif() endif() +# Allocation strategy (E11). Currently only "static" ships — internal static +# pools allocate instance bookkeeping for stateful classes. A future epic +# may add "dynamic" (heap-backed). Selecting "dynamic" now errors hard so +# downstream builds fail fast rather than silently linking against missing +# TUs. +set(SOLIDSYSLOG_ALLOCATION_STRATEGY "static" CACHE STRING + "Allocation strategy for instance bookkeeping: static (internal pools) or dynamic (heap, not yet shipped)") +set_property(CACHE SOLIDSYSLOG_ALLOCATION_STRATEGY PROPERTY STRINGS static dynamic) +if(SOLIDSYSLOG_ALLOCATION_STRATEGY STREQUAL "dynamic") + message(FATAL_ERROR + "SOLIDSYSLOG_ALLOCATION_STRATEGY=dynamic is reserved for a future epic; " + "no dynamic TUs ship yet. Set SOLIDSYSLOG_ALLOCATION_STRATEGY=static (the default).") +elseif(NOT SOLIDSYSLOG_ALLOCATION_STRATEGY STREQUAL "static") + message(FATAL_ERROR + "SOLIDSYSLOG_ALLOCATION_STRATEGY must be 'static' (got: '${SOLIDSYSLOG_ALLOCATION_STRATEGY}').") +endif() + # Build-time tunables (E21: Port-Time Configurability). Integrators override # library defaults by setting SOLIDSYSLOG_USER_TUNABLES_FILE to the absolute # path of their own header containing #define lines for whichever tunables diff --git a/Core/Interface/SolidSyslogCircularBuffer.h b/Core/Interface/SolidSyslogCircularBuffer.h index 0b5fcd99..767f8f69 100644 --- a/Core/Interface/SolidSyslogCircularBuffer.h +++ b/Core/Interface/SolidSyslogCircularBuffer.h @@ -13,29 +13,19 @@ EXTERN_C_BEGIN struct SolidSyslogBuffer; struct SolidSyslogMutex; - typedef size_t SolidSyslogCircularBufferStorage; - enum { - SOLIDSYSLOG_CIRCULAR_BUFFER_OVERHEAD = 7, SOLIDSYSLOG_CIRCULAR_BUFFER_HEADER_BYTES = sizeof(uint16_t) }; -/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -- cannot compute array size as a constexpr in C */ -#define SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE_BYTES(ringBytes) \ - (SOLIDSYSLOG_CIRCULAR_BUFFER_OVERHEAD + \ - (((ringBytes) + sizeof(SolidSyslogCircularBufferStorage) - 1U) / sizeof(SolidSyslogCircularBufferStorage))) - -/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -- cannot compute array size as a constexpr in C */ -#define SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE(maxMessages) \ - SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE_BYTES( \ - (size_t) (maxMessages) * (SOLIDSYSLOG_MAX_MESSAGE_SIZE + SOLIDSYSLOG_CIRCULAR_BUFFER_HEADER_BYTES) \ - ) +/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -- C array-size const-expr; not a function-like expression */ +#define SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(maxMessages) \ + ((maxMessages) * (SOLIDSYSLOG_MAX_MESSAGE_SIZE + SOLIDSYSLOG_CIRCULAR_BUFFER_HEADER_BYTES)) struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( - SolidSyslogCircularBufferStorage * storage, - size_t storageBytes, - struct SolidSyslogMutex* mutex + struct SolidSyslogMutex* mutex, + uint8_t* ring, + size_t ringBytes ); void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer * base); diff --git a/Core/Source/CMakeLists.txt b/Core/Source/CMakeLists.txt index 554df2a5..abb990e6 100644 --- a/Core/Source/CMakeLists.txt +++ b/Core/Source/CMakeLists.txt @@ -8,6 +8,7 @@ set(SOURCES SolidSyslogMetaSd.c SolidSyslogNullBuffer.c SolidSyslogCircularBuffer.c + SolidSyslogCircularBufferStatic.c SolidSyslogMutex.c SolidSyslogNullMutex.c SolidSyslogSender.c diff --git a/Core/Source/SolidSyslogCircularBuffer.c b/Core/Source/SolidSyslogCircularBuffer.c index d679f1b2..320c3282 100644 --- a/Core/Source/SolidSyslogCircularBuffer.c +++ b/Core/Source/SolidSyslogCircularBuffer.c @@ -6,7 +6,7 @@ #include #include "SolidSyslogBufferDefinition.h" -#include "SolidSyslogMacros.h" +#include "SolidSyslogCircularBufferPrivate.h" #include "SolidSyslogMutex.h" #include "SolidSyslogTunables.h" @@ -15,28 +15,9 @@ enum HEADER_BYTES = SOLIDSYSLOG_CIRCULAR_BUFFER_HEADER_BYTES }; -struct SolidSyslogCircularBuffer -{ - struct SolidSyslogBuffer Base; - struct SolidSyslogMutex* Mutex; - size_t Capacity; - size_t Head; - size_t Tail; - size_t WrapPoint; - uint8_t Storage[]; -}; - -SOLIDSYSLOG_STATIC_ASSERT( - sizeof(struct SolidSyslogCircularBuffer) == - (SOLIDSYSLOG_CIRCULAR_BUFFER_OVERHEAD * sizeof(SolidSyslogCircularBufferStorage)), - "SOLIDSYSLOG_CIRCULAR_BUFFER_OVERHEAD does not match struct layout" -); - static bool CircularBuffer_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead); static void CircularBuffer_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); -static inline struct SolidSyslogCircularBuffer* CircularBuffer_SelfFromStorage(SolidSyslogCircularBufferStorage* storage -); static inline struct SolidSyslogCircularBuffer* CircularBuffer_SelfFromBase(struct SolidSyslogBuffer* base); static inline bool CircularBuffer_IsEmpty(const struct SolidSyslogCircularBuffer* self); @@ -51,44 +32,33 @@ static inline void CircularBuffer_StoreRecord(struct SolidSyslogCircularBuffer* static inline void CircularBuffer_LoadRecord(struct SolidSyslogCircularBuffer* self, void* data, size_t* bytesRead); static inline size_t CircularBuffer_PeekRecordSize(const struct SolidSyslogCircularBuffer* self); -struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( - SolidSyslogCircularBufferStorage* storage, - size_t storageBytes, - struct SolidSyslogMutex* mutex +void CircularBuffer_Initialise( + struct SolidSyslogCircularBuffer* self, + struct SolidSyslogMutex* mutex, + uint8_t* ring, + size_t ringBytes ) { - struct SolidSyslogCircularBuffer* self = CircularBuffer_SelfFromStorage(storage); self->Base.Read = CircularBuffer_Read; self->Base.Write = CircularBuffer_Write; self->Mutex = mutex; - self->Capacity = storageBytes - sizeof(struct SolidSyslogCircularBuffer); + self->Ring = ring; + self->Capacity = ringBytes; CircularBuffer_ResetToStart(self); - return &self->Base; -} - -static inline struct SolidSyslogCircularBuffer* CircularBuffer_SelfFromStorage(SolidSyslogCircularBufferStorage* storage -) -{ - return (struct SolidSyslogCircularBuffer*) storage; } -void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) +void CircularBuffer_Cleanup(struct SolidSyslogCircularBuffer* self) { - struct SolidSyslogCircularBuffer* self = CircularBuffer_SelfFromBase(base); self->Base.Read = NULL; self->Base.Write = NULL; self->Mutex = NULL; + self->Ring = NULL; self->Capacity = 0; self->Head = 0; self->Tail = 0; self->WrapPoint = 0; } -static inline struct SolidSyslogCircularBuffer* CircularBuffer_SelfFromBase(struct SolidSyslogBuffer* base) -{ - return (struct SolidSyslogCircularBuffer*) base; -} - static bool CircularBuffer_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead) { struct SolidSyslogCircularBuffer* self = CircularBuffer_SelfFromBase(base); @@ -114,6 +84,11 @@ static bool CircularBuffer_Read(struct SolidSyslogBuffer* base, void* data, size return delivered; } +static inline struct SolidSyslogCircularBuffer* CircularBuffer_SelfFromBase(struct SolidSyslogBuffer* base) +{ + return (struct SolidSyslogCircularBuffer*) base; +} + static inline bool CircularBuffer_IsEmpty(const struct SolidSyslogCircularBuffer* self) { return self->Head == self->Tail; @@ -133,13 +108,13 @@ static inline void CircularBuffer_ConsumeWrapMarker(struct SolidSyslogCircularBu static inline size_t CircularBuffer_PeekRecordSize(const struct SolidSyslogCircularBuffer* self) { /* Little-endian read of the 2-byte length header out of the uint8_t ring. */ - return ((size_t) self->Storage[self->Head]) | (((size_t) self->Storage[self->Head + 1U]) << 8U); + return ((size_t) self->Ring[self->Head]) | (((size_t) self->Ring[self->Head + 1U]) << 8U); } static inline void CircularBuffer_LoadRecord(struct SolidSyslogCircularBuffer* self, void* data, size_t* bytesRead) { size_t recordSize = CircularBuffer_PeekRecordSize(self); - (void) memcpy(data, &self->Storage[self->Head + HEADER_BYTES], recordSize); + (void) memcpy(data, &self->Ring[self->Head + HEADER_BYTES], recordSize); self->Head += HEADER_BYTES + recordSize; *bytesRead = recordSize; } @@ -209,8 +184,8 @@ static inline void CircularBuffer_WrapTail(struct SolidSyslogCircularBuffer* sel static inline void CircularBuffer_StoreRecord(struct SolidSyslogCircularBuffer* self, const void* data, size_t size) { /* Little-endian write of the 2-byte length header into the uint8_t ring. */ - self->Storage[self->Tail] = (uint8_t) (size & 0xFFU); - self->Storage[self->Tail + 1U] = (uint8_t) ((size >> 8U) & 0xFFU); - (void) memcpy(&self->Storage[self->Tail + HEADER_BYTES], data, size); + self->Ring[self->Tail] = (uint8_t) (size & 0xFFU); + self->Ring[self->Tail + 1U] = (uint8_t) ((size >> 8U) & 0xFFU); + (void) memcpy(&self->Ring[self->Tail + HEADER_BYTES], data, size); self->Tail += HEADER_BYTES + size; } diff --git a/Core/Source/SolidSyslogCircularBufferPrivate.h b/Core/Source/SolidSyslogCircularBufferPrivate.h new file mode 100644 index 00000000..a4a8fd82 --- /dev/null +++ b/Core/Source/SolidSyslogCircularBufferPrivate.h @@ -0,0 +1,30 @@ +#ifndef SOLIDSYSLOGCIRCULARBUFFERPRIVATE_H +#define SOLIDSYSLOGCIRCULARBUFFERPRIVATE_H + +#include +#include + +#include "SolidSyslogBufferDefinition.h" + +struct SolidSyslogMutex; + +struct SolidSyslogCircularBuffer +{ + struct SolidSyslogBuffer Base; + struct SolidSyslogMutex* Mutex; + uint8_t* Ring; + size_t Capacity; + size_t Head; + size_t Tail; + size_t WrapPoint; +}; + +void CircularBuffer_Initialise( + struct SolidSyslogCircularBuffer* self, + struct SolidSyslogMutex* mutex, + uint8_t* ring, + size_t ringBytes +); +void CircularBuffer_Cleanup(struct SolidSyslogCircularBuffer* self); + +#endif /* SOLIDSYSLOGCIRCULARBUFFERPRIVATE_H */ diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c new file mode 100644 index 00000000..ca200bcb --- /dev/null +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -0,0 +1,30 @@ +#include "SolidSyslogCircularBuffer.h" + +#include +#include + +#include "SolidSyslogBufferDefinition.h" +#include "SolidSyslogCircularBufferPrivate.h" + +struct SolidSyslogMutex; + +static struct SolidSyslogCircularBuffer Instance; + +struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( + struct SolidSyslogMutex* mutex, + uint8_t* ring, + size_t ringBytes +) +{ + CircularBuffer_Initialise(&Instance, mutex, ring, ringBytes); + return &Instance.Base; +} + +/* cppcheck-suppress constParameter -- public API is non-const (mutating Destroy); next commit's pool ownership check exercises base for identity comparison and Cleanup mutates through it. */ +void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) +{ + if (base == &Instance.Base) + { + CircularBuffer_Cleanup(&Instance); + } +} diff --git a/README.md b/README.md index 1ab7da22..be2e0949 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Public headers are split by audience (Interface Segregation Principle): - **`SolidSyslogConfigLock.h`** — optional setup-time lock injection (`SolidSyslog_SetConfigLock(lockFn, unlockFn)`); wraps library-internal pool slot walks so multi-task setup is safe. Defaults are no-ops, so single-task systems can ignore it. Integrators on RTOS / multi-core wire `taskENTER_CRITICAL`, a static `pthread_mutex_t`, `EnterCriticalSection`, or a spinlock pair - **`SolidSyslogSenderDefinition.h`** / **`SolidSyslogBufferDefinition.h`** — extension points for custom senders and buffers - **`SolidSyslogNullBuffer.h`** — direct-send buffer for single-task systems -- **`SolidSyslogCircularBuffer.h`** — portable ring buffer with caller-allocated storage and an injected `SolidSyslogMutex` (`SolidSyslogPosixMutex` / `SolidSyslogWindowsMutex` / `SolidSyslogFreeRtosMutex` / `SolidSyslogNullMutex` / your own); the cross-platform threaded buffer +- **`SolidSyslogCircularBuffer.h`** — portable ring buffer with caller-allocated ring memory and an injected `SolidSyslogMutex` (`SolidSyslogPosixMutex` / `SolidSyslogWindowsMutex` / `SolidSyslogFreeRtosMutex` / `SolidSyslogNullMutex` / your own); the cross-platform threaded buffer. Instance bookkeeping lives in a library-internal static pool (E11) - **`SolidSyslogPosixMessageQueueBuffer.h`** — thread-safe POSIX message queue buffer - **`SolidSyslogUdpSender.h`** — UDP transport (RFC 5426) - **`SolidSyslogStreamSender.h`** — octet-framed syslog (RFC 6587) over any Stream. Note: RFC 6587 diff --git a/SKILL.md b/SKILL.md index b4e19a4b..457bbbe3 100644 --- a/SKILL.md +++ b/SKILL.md @@ -70,7 +70,7 @@ Test progression follows ZOMBIES order. - Dependency injection for transport, buffering, clock, hostname, allocator - Buffer abstraction decouples formatting from sending: `SolidSyslog_Log` writes to buffer, `SolidSyslog_Service` reads from buffer and sends. Implementations: NullBuffer (direct send, - single-task), CircularBuffer (portable ring with caller-allocated storage and an injected + single-task), CircularBuffer (portable ring with caller-allocated ring memory and an injected `SolidSyslogMutex` — Posix/Windows/Null mutex shipped, integrators can plug their own RTOS primitive), PosixMessageQueueBuffer (POSIX message queue, used by the Linux Threaded example) - Null object pattern throughout (NullBuffer is the buffer null object) diff --git a/Tests/SolidSyslogBlockStoreDrainOrderingTest.cpp b/Tests/SolidSyslogBlockStoreDrainOrderingTest.cpp index 8873a248..c4db0c67 100644 --- a/Tests/SolidSyslogBlockStoreDrainOrderingTest.cpp +++ b/Tests/SolidSyslogBlockStoreDrainOrderingTest.cpp @@ -223,7 +223,7 @@ TEST_GROUP_BASE(ServiceDrainInterleave, DrainTestFixtureBase) * reproducer; CircularBuffer is FIFO so all messages are retained * until Service drains them (unlike BufferFake which only keeps * the last one). */ - SolidSyslogCircularBufferStorage bufferStorage[SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE(16)] = {}; + uint8_t bufferRing[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(16)] = {}; SolidSyslogBlockStoreStorage storeStorage = {}; struct SolidSyslogStore* store = nullptr; struct SolidSyslogMutex* mutex = nullptr; @@ -234,7 +234,7 @@ TEST_GROUP_BASE(ServiceDrainInterleave, DrainTestFixtureBase) { setupBlockDeviceAndPolicy(); mutex = SolidSyslogNullMutex_Create(); - buffer = SolidSyslogCircularBuffer_Create(bufferStorage, sizeof(bufferStorage), mutex); + buffer = SolidSyslogCircularBuffer_Create(mutex, bufferRing, sizeof(bufferRing)); SenderSpy_Init(spy); } diff --git a/Tests/SolidSyslogCircularBufferTest.cpp b/Tests/SolidSyslogCircularBufferTest.cpp index a35967e6..f57e7afe 100644 --- a/Tests/SolidSyslogCircularBufferTest.cpp +++ b/Tests/SolidSyslogCircularBufferTest.cpp @@ -15,7 +15,7 @@ enum // clang-format off TEST_GROUP(SolidSyslogCircularBuffer) { - SolidSyslogCircularBufferStorage storage[SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE(TEST_MAX_MESSAGES)]; + uint8_t ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(TEST_MAX_MESSAGES)]; struct SolidSyslogBuffer* buffer = nullptr; char readData[SOLIDSYSLOG_MAX_MESSAGE_SIZE]; // cppcheck-suppress variableScope -- member of TEST_GROUP; scope managed by CppUTest macro @@ -24,7 +24,7 @@ TEST_GROUP(SolidSyslogCircularBuffer) void setup() override { // cppcheck-suppress unreadVariable -- used across TEST_GROUP methods; cppcheck does not model CppUTest macros - buffer = SolidSyslogCircularBuffer_Create(storage, sizeof(storage), SolidSyslogNullMutex_Create()); + buffer = SolidSyslogCircularBuffer_Create(SolidSyslogNullMutex_Create(), ring, sizeof(ring)); readSize = 0; } @@ -46,11 +46,6 @@ TEST(SolidSyslogCircularBuffer, CreateDestroyDoesNotCrash) { } -TEST(SolidSyslogCircularBuffer, HandleEqualsStorageAddress) -{ - POINTERS_EQUAL(&storage, buffer); -} - TEST(SolidSyslogCircularBuffer, ReadFromEmptyReturnsFalse) { CHECK_FALSE(SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize)); @@ -170,7 +165,7 @@ TEST(SolidSyslogCircularBuffer, WriteExceedingMaxMessageSizeIsDropped) // clang-format off TEST_GROUP(SolidSyslogCircularBufferMutex) { - SolidSyslogCircularBufferStorage storage[SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE(TEST_MAX_MESSAGES)]; + uint8_t ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(TEST_MAX_MESSAGES)]; struct SolidSyslogBuffer* buffer = nullptr; char readData[SOLIDSYSLOG_MAX_MESSAGE_SIZE]; // cppcheck-suppress variableScope -- member of TEST_GROUP; scope managed by CppUTest macro @@ -179,7 +174,7 @@ TEST_GROUP(SolidSyslogCircularBufferMutex) void setup() override { // cppcheck-suppress unreadVariable -- used across TEST_GROUP methods; cppcheck does not model CppUTest macros - buffer = SolidSyslogCircularBuffer_Create(storage, sizeof(storage), MutexFake_Create()); + buffer = SolidSyslogCircularBuffer_Create(MutexFake_Create(), ring, sizeof(ring)); readSize = 0; } @@ -217,7 +212,7 @@ enum // clang-format off TEST_GROUP(SolidSyslogCircularBufferSmallRing) { - SolidSyslogCircularBufferStorage storage[SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE_BYTES(SMALL_RING_BYTES)]; + uint8_t ring[SMALL_RING_BYTES]; struct SolidSyslogBuffer* buffer = nullptr; char readData[SMALL_RING_BYTES]; // cppcheck-suppress variableScope -- member of TEST_GROUP; scope managed by CppUTest macro @@ -226,7 +221,7 @@ TEST_GROUP(SolidSyslogCircularBufferSmallRing) void setup() override { // cppcheck-suppress unreadVariable -- used across TEST_GROUP methods; cppcheck does not model CppUTest macros - buffer = SolidSyslogCircularBuffer_Create(storage, sizeof(storage), SolidSyslogNullMutex_Create()); + buffer = SolidSyslogCircularBuffer_Create(SolidSyslogNullMutex_Create(), ring, sizeof(ring)); readSize = 0; } diff --git a/Tests/SolidSyslogTest.cpp b/Tests/SolidSyslogTest.cpp index 7fa9526e..db495a50 100644 --- a/Tests/SolidSyslogTest.cpp +++ b/Tests/SolidSyslogTest.cpp @@ -1541,12 +1541,11 @@ TEST_GROUP(SolidSyslogServiceEagerDrain) void setup() override { - static SolidSyslogCircularBufferStorage bufferStorage[ - SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE_BYTES(BUFFER_BYTES)]; + static uint8_t bufferRing[BUFFER_BYTES]; fakeSender = SenderFake_Create(); circularBuffer = SolidSyslogCircularBuffer_Create( - bufferStorage, sizeof(bufferStorage), SolidSyslogNullMutex_Create()); + SolidSyslogNullMutex_Create(), bufferRing, sizeof(bufferRing)); fakeStore = StoreFake_Create(); SolidSyslogConfig serviceConfig = {}; diff --git a/docs/NAMING.md b/docs/NAMING.md index 592dfb2e..32fb4c21 100644 --- a/docs/NAMING.md +++ b/docs/NAMING.md @@ -322,31 +322,37 @@ static bool CircularBuffer_Read(struct SolidSyslogBuffer* base, #### The storage cast: `_SelfFromStorage` -`_Create` functions take caller-supplied opaque storage and re-interpret -it as the concrete struct. The same convention applies — one named -`static inline` helper per class: +For classes still on the caller-supplied-storage pattern (BlockStore, +the Posix/Windows/FreeRTOS mutexes and streams, …), `_Create` takes +opaque storage and re-interprets it as the concrete struct. The same +convention applies — one named `static inline` helper per class: ```c -static inline struct SolidSyslogCircularBuffer* -CircularBuffer_SelfFromStorage(SolidSyslogCircularBufferStorage* storage) +static inline struct SolidSyslogBlockStore* +BlockStore_SelfFromStorage(SolidSyslogBlockStoreStorage* storage) { - return (struct SolidSyslogCircularBuffer*) storage; + return (struct SolidSyslogBlockStore*) storage; } ``` `_Create` becomes: ```c -struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( - SolidSyslogCircularBufferStorage* storage, ... +struct SolidSyslogStore* SolidSyslogBlockStore_Create( + SolidSyslogBlockStoreStorage* storage, ... ) { - struct SolidSyslogCircularBuffer* self = CircularBuffer_SelfFromStorage(storage); + struct SolidSyslogBlockStore* self = BlockStore_SelfFromStorage(storage); ... return &self->Base; } ``` +Classes migrated under E11 (currently only `SolidSyslogCircularBuffer`) +no longer use this cast — their instance struct lives in a +library-internal static pool, and `_Create` returns a slot pointer +without any storage cast. + Helpers are named per Tier 2 (`Class_Function`, `static inline`, no `SolidSyslog` prefix). Placement follows the function-ordering rule: forward-declared with the other helpers at the top of the file, diff --git a/docs/iec62443.md b/docs/iec62443.md index 5366d02c..72f58c20 100644 --- a/docs/iec62443.md +++ b/docs/iec62443.md @@ -139,7 +139,7 @@ For resource-constrained targets, SL1 or SL2 can operate with: `SolidSyslogCircularBuffer` is the portable choice for the producer / consumer seam between the application thread (calling `Log()`) and the service thread -(calling `Service()`) — caller-allocated storage, mutex injected via the +(calling `Service()`) — caller-allocated ring memory, mutex injected via the `SolidSyslogMutex` vtable, no POSIX dependency. Wrap your RTOS mutex primitive in the vtable and you're done. `SolidSyslogPosixMessageQueueBuffer` remains available where a POSIX `mqueue` is preferred for kernel-level back-pressure diff --git a/docs/misra-deviations.md b/docs/misra-deviations.md index 086197f0..9d621980 100644 --- a/docs/misra-deviations.md +++ b/docs/misra-deviations.md @@ -160,11 +160,18 @@ project constraints: | Pass-by-value structs | Doubles the parameter footprint of every `_Create`; breaks the vtable indirection that decouples Core from Platform. | `Address.h` (Strict tier, opaque `SolidSyslogAddress`) and every -caller-storage class (BlockStore, CircularBuffer, Formatter, the +caller-storage class (BlockStore, Formatter, the FreeRTOS/Posix/Windows mutexes and streams, …) hit the same three rules for the same structural reason — one deviation document covers all of them. +Note that `SolidSyslogCircularBuffer` moved off this pattern under +E11 (S11.01): the instance struct now lives in a library-internal +static pool, and the caller supplies only the ring memory as +plain `uint8_t* ring, size_t ringBytes`. No `void*` storage cast on +the instance; the ring pointer is held untyped inside the impl +struct. + ### Risk and mitigation - **Type safety** — Each `_Create` is the only place in the library @@ -325,9 +332,8 @@ Project owner — David Cozens. Recorded under ### Deviation -`struct SolidSyslogFormatter` and `struct SolidSyslogCircularBuffer` -each end with a flexible array member that holds the caller-supplied -backing storage: +`struct SolidSyslogFormatter` ends with a flexible array member that +holds the caller-supplied backing storage: ```c struct SolidSyslogFormatter @@ -339,14 +345,17 @@ struct SolidSyslogFormatter ### Scope -Two classes only: +One class only: - `Core/Source/SolidSyslogFormatter.c` -- `Core/Source/SolidSyslogCircularBuffer.c` + +`SolidSyslogCircularBuffer` used to share this shape but moved off +it under E11 (S11.01) — its instance struct now holds an external +ring pointer rather than a trailing FAM. ### Rationale -These two classes implement the variable-size variant of the +The Formatter implements the variable-size variant of the caller-supplied-storage pattern (D.002). The integrator declares a storage buffer of arbitrary size (with a minimum enforced by `_Static_assert`), and the class lives inside that storage — @@ -792,18 +801,17 @@ Project owner — David Cozens. Recorded under ### Deviation -`Core/Interface/SolidSyslogCircularBuffer.h` declares two function-like -macros — `SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE_BYTES` and -`SOLIDSYSLOG_CIRCULAR_BUFFER_STORAGE_SIZE` — that integrator code uses -to size caller-supplied storage. cppcheck-misra runs only over the -Strict tier (`Core/Source/`) and Pragmatic tier (`Platform/*/Source/`); +`Core/Interface/SolidSyslogCircularBuffer.h` declares one function-like +macro — `SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES` — that integrator code +uses to size caller-supplied ring memory. cppcheck-misra runs only over +the Strict tier (`Core/Source/`) and Pragmatic tier (`Platform/*/Source/`); the actual consumers live under `Tests/` (Consistency-only tier) and `Bdd/Targets/` (Out of scope) and are therefore invisible to the checker. ### Scope -`Core/Interface/SolidSyslogCircularBuffer.h` — two macro definitions. +`Core/Interface/SolidSyslogCircularBuffer.h` — one macro definition. A future per-component sweep may surface similar findings on other public API macros (per the tier model, MISRA enforcement does not cross @@ -817,17 +825,14 @@ The macros *are* used — by integrators in `Tests/` and `Bdd/Targets/`. Verified by `grep` over the tree: ``` -Tests/SolidSyslogCircularBufferTest.cpp — both macros -Tests/SolidSyslogTest.cpp — _BYTES form -Tests/SolidSyslogBlockStoreDrainOrderingTest.cpp — _SIZE form -Bdd/Targets/Windows/BddTargetWindows.c — _SIZE form -Bdd/Targets/FreeRtos/main.c — _SIZE form +Tests/SolidSyslogCircularBufferTest.cpp — RING_BYTES +Tests/SolidSyslogBlockStoreDrainOrderingTest.cpp — RING_BYTES +Bdd/Targets/Windows/BddTargetWindows.c — RING_BYTES +Bdd/Targets/FreeRtos/main.c — RING_BYTES ``` -The S10.05 audit's verdict ("Two declared macros that are never used. -Delete them.") was incorrect — the macros are part of the public API; -deleting them would force every integrator to compute storage size by -hand. +The macro is part of the public API; integrators use it to size +caller-supplied ring memory in messages. The alternatives all regress: diff --git a/misra_suppressions.txt b/misra_suppressions.txt index 21255891..e4711127 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -24,7 +24,6 @@ misra-c2012-11.2:Platform/Windows/Source/SolidSyslogAddressInternal.h:23 misra-c2012-11.3:Core/Interface/SolidSyslogFormatter.h:30 misra-c2012-11.3:Core/Source/SolidSyslogBlockStore.c:82 misra-c2012-11.3:Core/Source/SolidSyslogBlockStore.c:165 -misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:72 misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:89 misra-c2012-11.3:Core/Source/SolidSyslogFileBlockDevice.c:118 misra-c2012-11.3:Core/Source/SolidSyslogFileBlockDevice.c:146 @@ -86,7 +85,7 @@ misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:350 # See docs/misra-deviations.md#d003 misra-c2012-5.7:Core/Interface/SolidSyslogAddress.h:10 misra-c2012-5.7:Core/Interface/SolidSyslogBlockStore.h:52 -misra-c2012-5.7:Core/Interface/SolidSyslogCircularBuffer.h:19 +misra-c2012-5.7:Core/Interface/SolidSyslogCircularBuffer.h:17 misra-c2012-5.7:Core/Interface/SolidSyslogEndpoint.h:11 misra-c2012-5.7:Core/Interface/SolidSyslogFileBlockDevice.h:14 misra-c2012-5.7:Core/Interface/SolidSyslogFormatter.h:14 @@ -97,10 +96,10 @@ misra-c2012-5.7:Core/Interface/SolidSyslogTransport.h:5 misra-c2012-5.7:Core/Interface/SolidSyslogUdpPayload.h:15 misra-c2012-5.7:Core/Source/BlockSequence.c:6 misra-c2012-5.7:Core/Source/BlockSequence.c:92 +misra-c2012-5.7:Core/Source/SolidSyslogCircularBuffer.c:14 misra-c2012-5.7:Core/Source/RecordStore.c:9 misra-c2012-5.7:Core/Source/RecordStore.h:14 misra-c2012-5.7:Core/Source/SolidSyslog.c:26 -misra-c2012-5.7:Core/Source/SolidSyslogCircularBuffer.c:14 misra-c2012-5.7:Core/Source/SolidSyslogCrc16.c:12 misra-c2012-5.7:Core/Source/SolidSyslogCrc16Policy.c:10 misra-c2012-5.7:Core/Source/SolidSyslogFileBlockDevice.c:14 @@ -149,7 +148,6 @@ misra-c2012-18.4:Core/Source/RecordStore.c:47 # D.005 — Rule 18.7: flexible array members # See docs/misra-deviations.md#d005 -misra-c2012-18.7:Core/Source/SolidSyslogCircularBuffer.c:26 misra-c2012-18.7:Core/Source/SolidSyslogFormatter.c:12 # D.006 — Rule 1.4: C11 @@ -183,7 +181,7 @@ misra-c2012-21.6:Platform/Windows/Source/SolidSyslogWindowsFile.c:8 # D.010 — Rule 2.4: anonymous enum used as named-constant container # See docs/misra-deviations.md#d010 misra-c2012-2.4:Core/Interface/SolidSyslogBlockStore.h:52 -misra-c2012-2.4:Core/Interface/SolidSyslogCircularBuffer.h:19 +misra-c2012-2.4:Core/Interface/SolidSyslogCircularBuffer.h:17 misra-c2012-2.4:Core/Interface/SolidSyslogSecurityPolicyDefinition.h:13 misra-c2012-2.4:Core/Interface/SolidSyslogTransport.h:5 misra-c2012-2.4:Core/Source/BlockSequence.c:6 @@ -197,6 +195,5 @@ misra-c2012-20.10:Core/Source/SolidSyslogMacros.h:9 # D.012 — Rule 2.5: public API macros consumed outside cppcheck-misra scope # See docs/misra-deviations.md#d012 -misra-c2012-2.5:Core/Interface/SolidSyslogCircularBuffer.h:25 -misra-c2012-2.5:Core/Interface/SolidSyslogCircularBuffer.h:30 +misra-c2012-2.5:Core/Interface/SolidSyslogCircularBuffer.h:22 From 95544e5c27f1337c4352069703c61b8ec224c111 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 18:16:52 +0100 Subject: [PATCH 02/31] feat: S11.01 pool exhaustion + LockConfig wrap + tunable; useStlAlgorithm disabled project-wide Adds the second half of the pool mechanics now that there is a real consumer of POOL_SIZE: pool walks (claim + release) wrap in SolidSyslog_LockConfig / _UnlockConfig, exhausted Create returns a class-private fallback singleton with no-op vtable, and emits SolidSyslog_Error(ERROR, "...pool exhausted..."). Tunable: - SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE (default 1U) added to SolidSyslogTunablesDefaults.h with the standard #ifndef / floor guard pattern + NOLINTNEXTLINE matching SOLIDSYSLOG_MAX_MESSAGE_SIZE. CMake / cppcheck: - Global --suppress=useStlAlgorithm added to CMAKE_CXX_CPPCHECK. The rule only ever fires on C++ test code (production is C, no STL), and replacing test loops with std::generate masks the side-effecting Create per iteration rather than expressing it. Replaces the per-loop inline suppression that the earlier attempt added. Tests: - New TEST_GROUP(SolidSyslogCircularBufferPool) covering: filling the pool returns distinct buffers + a distinct fallback on overflow, exhausted Create reports ERROR, fallback Read/Write are no-ops, Create and Destroy each wrap in LockConfig / UnlockConfig. - Tests are parameterised on SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE (loop POOL_SIZE Creates into a fixture array, then one more for the overflow) so they work at any pool-size tunable. Outstanding hygiene (next commit): - Cleanup currently takes struct SolidSyslogCircularBuffer*; will change to take struct SolidSyslogBuffer* and do SelfFromBase internally so Static.c no longer needs the constParameter suppression. - Extract Pool_TryClaim / Pool_Release / Slot_OwnsBase static-inlines in Static.c to express intent and DRY the two pool walks. - Improve test diagnostic messages: bare CHECK(...) replaced with CHECK_TEXT(...) or a CHECK_IS_FALLBACK(buf, pool) macro for the central pool-test assertion. --- CMakeLists.txt | 1 + Core/Interface/SolidSyslogTunablesDefaults.h | 22 ++++ Core/Source/SolidSyslogCircularBufferStatic.c | 67 ++++++++++-- Core/Source/SolidSyslogErrorMessages.h | 2 + Tests/SolidSyslogCircularBufferTest.cpp | 101 ++++++++++++++++++ 5 files changed, 187 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3816a70c..59513f2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ if(ENABLE_CPPCHECK) "--enable=warning,style,performance,portability" "--error-exitcode=1" "--suppress=missingIncludeSystem" + "--suppress=useStlAlgorithm" "--inline-suppr" "--std=c++17" ) diff --git a/Core/Interface/SolidSyslogTunablesDefaults.h b/Core/Interface/SolidSyslogTunablesDefaults.h index dad2c709..930efa3e 100644 --- a/Core/Interface/SolidSyslogTunablesDefaults.h +++ b/Core/Interface/SolidSyslogTunablesDefaults.h @@ -28,4 +28,26 @@ #error "SOLIDSYSLOG_MAX_MESSAGE_SIZE must be >= 64" #endif +/* + * Number of SolidSyslogCircularBuffer instances the library's internal + * static pool can simultaneously hold. Each instance is a small + * bookkeeping struct (vtable, mutex pointer, ring pointer, head/tail/wrap) + * — roughly 64 bytes on a 64-bit target, 32 on a 32-bit target. The + * caller's ring memory is separate (passed to _Create). + * + * Most integrators only ever create one CircularBuffer per process; + * default 1. Bump via SOLIDSYSLOG_USER_TUNABLES_FILE if the integrator + * needs multiple concurrent buffer instances. + * + * Floor: 1. Sub-floor values rejected at compile time. + */ +#ifndef SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE +/* NOLINTNEXTLINE(cppcoreguidelines-macro-usage) -- macro form required for preprocessor visibility (floor #if) and C array-size const-expr. */ +#define SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE 1U +#endif + +#if SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE < 1 +#error "SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE must be >= 1" +#endif + #endif /* SOLIDSYSLOG_TUNABLES_DEFAULTS_H */ diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index ca200bcb..3fb9676a 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -1,14 +1,30 @@ #include "SolidSyslogCircularBuffer.h" +#include #include #include #include "SolidSyslogBufferDefinition.h" #include "SolidSyslogCircularBufferPrivate.h" +#include "SolidSyslogConfigLock.h" +#include "SolidSyslogError.h" +#include "SolidSyslogErrorMessages.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogTunables.h" struct SolidSyslogMutex; -static struct SolidSyslogCircularBuffer Instance; +struct Slot +{ + struct SolidSyslogCircularBuffer Object; + bool InUse; +}; + +static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead); +static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); + +static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; +static struct SolidSyslogBuffer Fallback = { Fallback_Write, Fallback_Read }; struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( struct SolidSyslogMutex* mutex, @@ -16,15 +32,54 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( size_t ringBytes ) { - CircularBuffer_Initialise(&Instance, mutex, ring, ringBytes); - return &Instance.Base; + struct SolidSyslogBuffer* result = &Fallback; + SolidSyslog_LockConfig(); + for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) + { + if (!Pool[i].InUse) + { + Pool[i].InUse = true; + CircularBuffer_Initialise(&Pool[i].Object, mutex, ring, ringBytes); + result = &Pool[i].Object.Base; + break; + } + } + SolidSyslog_UnlockConfig(); + if (result == &Fallback) + { + SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED); + } + return result; } -/* cppcheck-suppress constParameter -- public API is non-const (mutating Destroy); next commit's pool ownership check exercises base for identity comparison and Cleanup mutates through it. */ +/* cppcheck-suppress constParameter -- public API is non-const for symmetry with every other SolidSyslog*_Destroy(struct SolidSyslogX* base); this TU's pool walk only compares the pointer, but callers pass the handle they own (and may free elsewhere). */ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { - if (base == &Instance.Base) + SolidSyslog_LockConfig(); + for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) { - CircularBuffer_Cleanup(&Instance); + if (Pool[i].InUse && (base == &Pool[i].Object.Base)) + { + CircularBuffer_Cleanup(&Pool[i].Object); + Pool[i].InUse = false; + break; + } } + SolidSyslog_UnlockConfig(); +} + +static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead) +{ + (void) base; + (void) data; + (void) maxSize; + *bytesRead = 0; + return false; +} + +static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size) +{ + (void) base; + (void) data; + (void) size; } diff --git a/Core/Source/SolidSyslogErrorMessages.h b/Core/Source/SolidSyslogErrorMessages.h index 89e61227..25f7c3f3 100644 --- a/Core/Source/SolidSyslogErrorMessages.h +++ b/Core/Source/SolidSyslogErrorMessages.h @@ -17,5 +17,7 @@ #define SOLIDSYSLOG_ERROR_MSG_UDPSENDER_CREATE_NULL_DATAGRAM "SolidSyslogUdpSender_Create config.Datagram is NULL" #define SOLIDSYSLOG_ERROR_MSG_UDPSENDER_CREATE_NULL_ENDPOINT "SolidSyslogUdpSender_Create config.Endpoint is NULL" #define SOLIDSYSLOG_ERROR_MSG_UDPSENDER_SEND_NULL_BUFFER "SolidSyslogUdpSender_Send called with NULL buffer" +#define SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED \ + "SolidSyslogCircularBuffer_Create pool exhausted; returning fallback buffer" #endif /* SOLIDSYSLOGERRORMESSAGES_H */ diff --git a/Tests/SolidSyslogCircularBufferTest.cpp b/Tests/SolidSyslogCircularBufferTest.cpp index f57e7afe..1bf64c12 100644 --- a/Tests/SolidSyslogCircularBufferTest.cpp +++ b/Tests/SolidSyslogCircularBufferTest.cpp @@ -1,11 +1,17 @@ #include #include "CppUTest/TestHarness.h" + +#include "ConfigLockFake.h" +#include "ErrorHandlerFake.h" #include "MutexFake.h" #include "SolidSyslogBuffer.h" #include "SolidSyslogCircularBuffer.h" #include "SolidSyslogNullMutex.h" #include "SolidSyslogTunables.h" +#include "TestUtils.h" + +using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-file scope only; brings ONCE/NEVER into scope for CALLED_FAKE enum { @@ -328,3 +334,98 @@ TEST(SolidSyslogCircularBufferSmallRing, WrappedBufferReadsAllRecordsInOrder) MEMCMP_EQUAL(d, readData, sizeof(d)); CHECK_FALSE(Read()); } + +// clang-format off +TEST_GROUP(SolidSyslogCircularBufferPool) +{ + uint8_t ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(1)]; + struct SolidSyslogMutex* mutex = nullptr; + struct SolidSyslogBuffer* pooled[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE] = {}; + struct SolidSyslogBuffer* overflow = nullptr; + + void setup() override + { + // cppcheck-suppress unreadVariable -- used across TEST_GROUP methods; cppcheck does not model CppUTest macros + mutex = SolidSyslogNullMutex_Create(); + } + + void teardown() override + { + for (auto* buffer : pooled) + { + SolidSyslogCircularBuffer_Destroy(buffer); + } + SolidSyslogCircularBuffer_Destroy(overflow); + SolidSyslogNullMutex_Destroy(); + ConfigLockFake_Uninstall(); + ErrorHandlerFake_Uninstall(); + } + + void FillPool() + { + for (auto*& slot : pooled) + { + slot = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + } + } +}; + +// clang-format on + +TEST(SolidSyslogCircularBufferPool, FillingPoolThenOverflowReturnsDistinctFallback) +{ + FillPool(); + overflow = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + + CHECK(overflow != nullptr); + for (auto* slot : pooled) + { + CHECK(slot != nullptr); + CHECK(overflow != slot); + } +} + +TEST(SolidSyslogCircularBufferPool, ExhaustedCreateReportsError) +{ + ErrorHandlerFake_Install(nullptr); + FillPool(); + + overflow = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, ErrorHandlerFake_LastSeverity()); +} + +TEST(SolidSyslogCircularBufferPool, FallbackWriteAndReadAreNoOps) +{ + FillPool(); + overflow = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + + SolidSyslogBuffer_Write(overflow, "hello", 5); + + char readBuffer[16] = {}; + size_t bytesRead = 99; + CHECK_FALSE(SolidSyslogBuffer_Read(overflow, readBuffer, sizeof(readBuffer), &bytesRead)); + LONGS_EQUAL(0, bytesRead); +} + +TEST(SolidSyslogCircularBufferPool, CreateAcquiresAndReleasesConfigLock) +{ + ConfigLockFake_Install(); + + pooled[0] = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + + CALLED_FAKE(ConfigLockFake_Lock, ONCE); + CALLED_FAKE(ConfigLockFake_Unlock, ONCE); +} + +TEST(SolidSyslogCircularBufferPool, DestroyAcquiresAndReleasesConfigLock) +{ + pooled[0] = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + ConfigLockFake_Install(); + + SolidSyslogCircularBuffer_Destroy(pooled[0]); + + CALLED_FAKE(ConfigLockFake_Lock, ONCE); + CALLED_FAKE(ConfigLockFake_Unlock, ONCE); +} From 19bd20c82c5cd573136c82c88264eac11de665c1 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 19:27:16 +0100 Subject: [PATCH 03/31] style: S11.01 clang-format pool migration files Pure formatting pass on CircularBuffer.h / .c / Private.h. The earlier S11.01 commits left aligned-column declarations that the project's clang-format rejects (PointerAlignment: Left, AlignConsecutiveDeclarations: None). No code changes. --- Core/Interface/SolidSyslogCircularBuffer.h | 2 +- Core/Source/SolidSyslogCircularBuffer.c | 6 +++--- Core/Source/SolidSyslogCircularBufferPrivate.h | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Core/Interface/SolidSyslogCircularBuffer.h b/Core/Interface/SolidSyslogCircularBuffer.h index 767f8f69..e221a45f 100644 --- a/Core/Interface/SolidSyslogCircularBuffer.h +++ b/Core/Interface/SolidSyslogCircularBuffer.h @@ -23,7 +23,7 @@ EXTERN_C_BEGIN ((maxMessages) * (SOLIDSYSLOG_MAX_MESSAGE_SIZE + SOLIDSYSLOG_CIRCULAR_BUFFER_HEADER_BYTES)) struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( - struct SolidSyslogMutex* mutex, + struct SolidSyslogMutex * mutex, uint8_t* ring, size_t ringBytes ); diff --git a/Core/Source/SolidSyslogCircularBuffer.c b/Core/Source/SolidSyslogCircularBuffer.c index 320c3282..3ef59eed 100644 --- a/Core/Source/SolidSyslogCircularBuffer.c +++ b/Core/Source/SolidSyslogCircularBuffer.c @@ -34,9 +34,9 @@ static inline size_t CircularBuffer_PeekRecordSize(const struct SolidSyslogCircu void CircularBuffer_Initialise( struct SolidSyslogCircularBuffer* self, - struct SolidSyslogMutex* mutex, - uint8_t* ring, - size_t ringBytes + struct SolidSyslogMutex* mutex, + uint8_t* ring, + size_t ringBytes ) { self->Base.Read = CircularBuffer_Read; diff --git a/Core/Source/SolidSyslogCircularBufferPrivate.h b/Core/Source/SolidSyslogCircularBufferPrivate.h index a4a8fd82..a20c6585 100644 --- a/Core/Source/SolidSyslogCircularBufferPrivate.h +++ b/Core/Source/SolidSyslogCircularBufferPrivate.h @@ -12,18 +12,18 @@ struct SolidSyslogCircularBuffer { struct SolidSyslogBuffer Base; struct SolidSyslogMutex* Mutex; - uint8_t* Ring; - size_t Capacity; - size_t Head; - size_t Tail; - size_t WrapPoint; + uint8_t* Ring; + size_t Capacity; + size_t Head; + size_t Tail; + size_t WrapPoint; }; void CircularBuffer_Initialise( struct SolidSyslogCircularBuffer* self, - struct SolidSyslogMutex* mutex, - uint8_t* ring, - size_t ringBytes + struct SolidSyslogMutex* mutex, + uint8_t* ring, + size_t ringBytes ); void CircularBuffer_Cleanup(struct SolidSyslogCircularBuffer* self); From 37b8d61f26f7fe83b5e2ed1c5d7b2030ef3687b3 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 19:27:32 +0100 Subject: [PATCH 04/31] feat: S11.01 Destroy reports WARNING on unknown handle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SolidSyslogCircularBuffer_Destroy now emits SolidSyslog_Error (SOLIDSYSLOG_SEVERITY_WARNING) when given a handle the pool never issued. Handles legitimately returned by _Create — pool-slot bases and the pool-exhaustion Fallback — remain silent. Drives the acceptance criterion from #392. Two tests in TEST_GROUP(SolidSyslogCircularBufferPool): - DestroyOfUnknownHandleReportsWarning pins the WARNING severity and message text via the existing ErrorHandlerFake. - DestroyOfFallbackHandleIsSilent pins the Fallback exception (otherwise every integrator who Destroys the buffer they were given would get a spurious warning when the pool was exhausted). Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 20 +++++++++--- Core/Source/SolidSyslogErrorMessages.h | 2 ++ Tests/SolidSyslogCircularBufferTest.cpp | 31 +++++++++++++++++-- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 3fb9676a..22b8c37a 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -17,19 +17,19 @@ struct SolidSyslogMutex; struct Slot { struct SolidSyslogCircularBuffer Object; - bool InUse; + bool InUse; }; static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead); static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); -static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; -static struct SolidSyslogBuffer Fallback = { Fallback_Write, Fallback_Read }; +static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; +static struct SolidSyslogBuffer Fallback = {Fallback_Write, Fallback_Read}; struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( struct SolidSyslogMutex* mutex, - uint8_t* ring, - size_t ringBytes + uint8_t* ring, + size_t ringBytes ) { struct SolidSyslogBuffer* result = &Fallback; @@ -55,6 +55,11 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( /* cppcheck-suppress constParameter -- public API is non-const for symmetry with every other SolidSyslog*_Destroy(struct SolidSyslogX* base); this TU's pool walk only compares the pointer, but callers pass the handle they own (and may free elsewhere). */ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { + if (base == &Fallback) + { + return; + } + bool released = false; SolidSyslog_LockConfig(); for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) { @@ -62,10 +67,15 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { CircularBuffer_Cleanup(&Pool[i].Object); Pool[i].InUse = false; + released = true; break; } } SolidSyslog_UnlockConfig(); + if (!released) + { + SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY); + } } static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead) diff --git a/Core/Source/SolidSyslogErrorMessages.h b/Core/Source/SolidSyslogErrorMessages.h index 25f7c3f3..e2529b20 100644 --- a/Core/Source/SolidSyslogErrorMessages.h +++ b/Core/Source/SolidSyslogErrorMessages.h @@ -19,5 +19,7 @@ #define SOLIDSYSLOG_ERROR_MSG_UDPSENDER_SEND_NULL_BUFFER "SolidSyslogUdpSender_Send called with NULL buffer" #define SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED \ "SolidSyslogCircularBuffer_Create pool exhausted; returning fallback buffer" +#define SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY \ + "SolidSyslogCircularBuffer_Destroy called with a handle not issued by this pool" #endif /* SOLIDSYSLOGERRORMESSAGES_H */ diff --git a/Tests/SolidSyslogCircularBufferTest.cpp b/Tests/SolidSyslogCircularBufferTest.cpp index 1bf64c12..d9005c75 100644 --- a/Tests/SolidSyslogCircularBufferTest.cpp +++ b/Tests/SolidSyslogCircularBufferTest.cpp @@ -6,8 +6,11 @@ #include "ErrorHandlerFake.h" #include "MutexFake.h" #include "SolidSyslogBuffer.h" +#include "SolidSyslogBufferDefinition.h" #include "SolidSyslogCircularBuffer.h" +#include "SolidSyslogErrorMessages.h" #include "SolidSyslogNullMutex.h" +#include "SolidSyslogPrival.h" #include "SolidSyslogTunables.h" #include "TestUtils.h" @@ -403,8 +406,8 @@ TEST(SolidSyslogCircularBufferPool, FallbackWriteAndReadAreNoOps) SolidSyslogBuffer_Write(overflow, "hello", 5); - char readBuffer[16] = {}; - size_t bytesRead = 99; + char readBuffer[16] = {}; + size_t bytesRead = 99; CHECK_FALSE(SolidSyslogBuffer_Read(overflow, readBuffer, sizeof(readBuffer), &bytesRead)); LONGS_EQUAL(0, bytesRead); } @@ -429,3 +432,27 @@ TEST(SolidSyslogCircularBufferPool, DestroyAcquiresAndReleasesConfigLock) CALLED_FAKE(ConfigLockFake_Lock, ONCE); CALLED_FAKE(ConfigLockFake_Unlock, ONCE); } + +TEST(SolidSyslogCircularBufferPool, DestroyOfUnknownHandleReportsWarning) +{ + ErrorHandlerFake_Install(nullptr); + struct SolidSyslogBuffer stranger = {}; + + SolidSyslogCircularBuffer_Destroy(&stranger); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity()); + STRCMP_EQUAL(SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY, ErrorHandlerFake_LastMessage()); +} + +TEST(SolidSyslogCircularBufferPool, DestroyOfFallbackHandleIsSilent) +{ + FillPool(); + overflow = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + ErrorHandlerFake_Install(nullptr); + + SolidSyslogCircularBuffer_Destroy(overflow); + overflow = nullptr; + + CALLED_FAKE(ErrorHandlerFake_Handle, NEVER); +} From b5f3cfd38c8d9b778f5569b07dbecfc6a8d2a4a2 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 19:31:11 +0100 Subject: [PATCH 05/31] refactor: S11.01 CircularBuffer_Cleanup takes base, drop suppression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleanup now takes struct SolidSyslogBuffer* and casts internally, matching the symmetry of every other vtable method on the class. *Static.c can hand it the integrator's handle directly, which removes the cppcheck constParameter complaint that previously needed an inline suppression on _Destroy. This is the pattern the rest of E11 will follow: Initialise takes the concrete pool slot (known shape inside *Static.c), Cleanup takes the public handle (unknown provenance from the caller's side). Adjusts the existing 11.3 line-pinned suppression for the SelfFromBase cast — the cast moved one line down inside the new Cleanup body. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBuffer.c | 3 ++- Core/Source/SolidSyslogCircularBufferPrivate.h | 2 +- Core/Source/SolidSyslogCircularBufferStatic.c | 3 +-- misra_suppressions.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBuffer.c b/Core/Source/SolidSyslogCircularBuffer.c index 3ef59eed..42840f6e 100644 --- a/Core/Source/SolidSyslogCircularBuffer.c +++ b/Core/Source/SolidSyslogCircularBuffer.c @@ -47,8 +47,9 @@ void CircularBuffer_Initialise( CircularBuffer_ResetToStart(self); } -void CircularBuffer_Cleanup(struct SolidSyslogCircularBuffer* self) +void CircularBuffer_Cleanup(struct SolidSyslogBuffer* base) { + struct SolidSyslogCircularBuffer* self = CircularBuffer_SelfFromBase(base); self->Base.Read = NULL; self->Base.Write = NULL; self->Mutex = NULL; diff --git a/Core/Source/SolidSyslogCircularBufferPrivate.h b/Core/Source/SolidSyslogCircularBufferPrivate.h index a20c6585..7ab378c9 100644 --- a/Core/Source/SolidSyslogCircularBufferPrivate.h +++ b/Core/Source/SolidSyslogCircularBufferPrivate.h @@ -25,6 +25,6 @@ void CircularBuffer_Initialise( uint8_t* ring, size_t ringBytes ); -void CircularBuffer_Cleanup(struct SolidSyslogCircularBuffer* self); +void CircularBuffer_Cleanup(struct SolidSyslogBuffer* base); #endif /* SOLIDSYSLOGCIRCULARBUFFERPRIVATE_H */ diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 22b8c37a..2f74641a 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -52,7 +52,6 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( return result; } -/* cppcheck-suppress constParameter -- public API is non-const for symmetry with every other SolidSyslog*_Destroy(struct SolidSyslogX* base); this TU's pool walk only compares the pointer, but callers pass the handle they own (and may free elsewhere). */ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { if (base == &Fallback) @@ -65,7 +64,7 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { if (Pool[i].InUse && (base == &Pool[i].Object.Base)) { - CircularBuffer_Cleanup(&Pool[i].Object); + CircularBuffer_Cleanup(base); Pool[i].InUse = false; released = true; break; diff --git a/misra_suppressions.txt b/misra_suppressions.txt index e4711127..2cd58ed0 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -24,7 +24,7 @@ misra-c2012-11.2:Platform/Windows/Source/SolidSyslogAddressInternal.h:23 misra-c2012-11.3:Core/Interface/SolidSyslogFormatter.h:30 misra-c2012-11.3:Core/Source/SolidSyslogBlockStore.c:82 misra-c2012-11.3:Core/Source/SolidSyslogBlockStore.c:165 -misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:89 +misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:90 misra-c2012-11.3:Core/Source/SolidSyslogFileBlockDevice.c:118 misra-c2012-11.3:Core/Source/SolidSyslogFileBlockDevice.c:146 misra-c2012-11.3:Core/Source/SolidSyslogFormatter.c:93 From 7d4e8419e7019ec5a9c389bc24f8d9d81ea26974 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 19:34:55 +0100 Subject: [PATCH 06/31] refactor: S11.01 extract pool helpers in CircularBufferStatic _Create and _Destroy now read as the lifecycle they describe: _Create -> LockConfig; claimed = TryClaim(...); UnlockConfig; if (!claimed) Error+Fallback; return claimed; _Destroy -> if (base != &Fallback) { LockConfig; released = Release(base); UnlockConfig; if (!released) Error(WARNING, unknown handle); } Three new file-scope static-inline helpers carry the intent: - CircularBuffer_TryClaim -> walk pool, claim first free slot - CircularBuffer_Release -> walk pool, release matching slot - CircularBuffer_SlotOwnsBase -> named predicate for the slot-walk match _Destroy is now single-exit fall-through, retiring a pre-existing MISRA 15.5 violation. cppcheck-misra count drops 88 -> 87. The lock/unlock pair still wraps every slot walk; the error reporting stays outside the critical section, matching the existing pattern. Helpers are forward-declared at the top and defined immediately beneath their first caller, matching SolidSyslogFileBlockDevice.c. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 69 +++++++++++++------ 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 2f74641a..f4e9e60d 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -23,6 +23,14 @@ struct Slot static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead); static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); +static inline struct SolidSyslogBuffer* CircularBuffer_TryClaim( + struct SolidSyslogMutex* mutex, + uint8_t* ring, + size_t ringBytes +); +static inline bool CircularBuffer_Release(struct SolidSyslogBuffer* base); +static inline bool CircularBuffer_SlotOwnsBase(const struct Slot* slot, const struct SolidSyslogBuffer* base); + static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; static struct SolidSyslogBuffer Fallback = {Fallback_Write, Fallback_Read}; @@ -32,49 +40,68 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( size_t ringBytes ) { - struct SolidSyslogBuffer* result = &Fallback; SolidSyslog_LockConfig(); - for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) + struct SolidSyslogBuffer* claimed = CircularBuffer_TryClaim(mutex, ring, ringBytes); + SolidSyslog_UnlockConfig(); + if (claimed == NULL) + { + SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED); + claimed = &Fallback; + } + return claimed; +} + +static inline struct SolidSyslogBuffer* CircularBuffer_TryClaim( + struct SolidSyslogMutex* mutex, + uint8_t* ring, + size_t ringBytes +) +{ + struct SolidSyslogBuffer* claimed = NULL; + for (size_t i = 0; (i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && (claimed == NULL); i++) { if (!Pool[i].InUse) { Pool[i].InUse = true; CircularBuffer_Initialise(&Pool[i].Object, mutex, ring, ringBytes); - result = &Pool[i].Object.Base; - break; + claimed = &Pool[i].Object.Base; } } - SolidSyslog_UnlockConfig(); - if (result == &Fallback) - { - SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED); - } - return result; + return claimed; } void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { - if (base == &Fallback) + if (base != &Fallback) { - return; + SolidSyslog_LockConfig(); + bool released = CircularBuffer_Release(base); + SolidSyslog_UnlockConfig(); + if (!released) + { + SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY); + } } +} + +static inline bool CircularBuffer_Release(struct SolidSyslogBuffer* base) +{ bool released = false; - SolidSyslog_LockConfig(); - for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) + for (size_t i = 0; (i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && !released; i++) { - if (Pool[i].InUse && (base == &Pool[i].Object.Base)) + if (Pool[i].InUse && CircularBuffer_SlotOwnsBase(&Pool[i], base)) { CircularBuffer_Cleanup(base); Pool[i].InUse = false; released = true; - break; } } - SolidSyslog_UnlockConfig(); - if (!released) - { - SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY); - } + return released; +} + +static inline bool CircularBuffer_SlotOwnsBase(const struct Slot* slot, const struct SolidSyslogBuffer* base) +{ + return base == &slot->Object.Base; } static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead) From 4a34777bd805bab3bf9bbe071932f006a96e02a4 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 19:39:41 +0100 Subject: [PATCH 07/31] refactor: S11.01 lock per slot probe, Initialise outside the lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous shape held the LockConfig critical section around the whole slot walk, including the CircularBuffer_Initialise call. That becomes a problem the moment another E11 class has a non-trivial Initialise (FatFs file open, TLS context setup, ...) or the integrator maps LockConfig to taskENTER_CRITICAL on FreeRTOS — interrupts would be disabled for the full Initialise. TryClaim now locks per slot probe: lock, check, claim (mark InUse), unlock. Initialise runs once, outside any critical section. The slot is safe to initialise lock-free because the InUse=true flag has already reserved it against parallel Creates, and no other task can yet hold the handle (we haven't returned from _Create). Release / _Destroy uses the same per-iteration locking but Cleanup stays inside the critical section: releasing the lock between match and Cleanup would let a concurrent Destroy of the same handle race, and releasing it after marking InUse=false would let a concurrent Create grab the slot and race Initialise vs Cleanup on the same memory. The asymmetry is intentional and worth documenting for the rest of E11: Create can offload Initialise outside the lock; Destroy cannot offload Cleanup without a third "Pending" slot state. For CircularBuffer specifically Cleanup is 8 stores; on classes where Cleanup is expensive, integrators should map LockConfig to a sleeping mutex (pthread_mutex_t, FreeRTOS Semaphore) rather than a critical section. The existing CreateAcquiresAndReleasesConfigLock / DestroyAcquires... tests still pass: at default SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE=1 "once per probed slot" equals "once total". A tunable-override preset with pool size > 1 would pin per-iteration locking observably; not adding one yet. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index f4e9e60d..42fd50df 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -40,9 +40,7 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( size_t ringBytes ) { - SolidSyslog_LockConfig(); struct SolidSyslogBuffer* claimed = CircularBuffer_TryClaim(mutex, ring, ringBytes); - SolidSyslog_UnlockConfig(); if (claimed == NULL) { SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED); @@ -57,15 +55,24 @@ static inline struct SolidSyslogBuffer* CircularBuffer_TryClaim( size_t ringBytes ) { - struct SolidSyslogBuffer* claimed = NULL; - for (size_t i = 0; (i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && (claimed == NULL); i++) + size_t claimedIndex = SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; + for (size_t i = 0; + (i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && (claimedIndex == SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE); + i++) { + SolidSyslog_LockConfig(); if (!Pool[i].InUse) { Pool[i].InUse = true; - CircularBuffer_Initialise(&Pool[i].Object, mutex, ring, ringBytes); - claimed = &Pool[i].Object.Base; + claimedIndex = i; } + SolidSyslog_UnlockConfig(); + } + struct SolidSyslogBuffer* claimed = NULL; + if (claimedIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) + { + CircularBuffer_Initialise(&Pool[claimedIndex].Object, mutex, ring, ringBytes); + claimed = &Pool[claimedIndex].Object.Base; } return claimed; } @@ -74,9 +81,7 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { if (base != &Fallback) { - SolidSyslog_LockConfig(); bool released = CircularBuffer_Release(base); - SolidSyslog_UnlockConfig(); if (!released) { SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY); @@ -89,12 +94,14 @@ static inline bool CircularBuffer_Release(struct SolidSyslogBuffer* base) bool released = false; for (size_t i = 0; (i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && !released; i++) { + SolidSyslog_LockConfig(); if (Pool[i].InUse && CircularBuffer_SlotOwnsBase(&Pool[i], base)) { CircularBuffer_Cleanup(base); Pool[i].InUse = false; released = true; } + SolidSyslog_UnlockConfig(); } return released; } From f5dccc85f217f4e7458340928f46bab993e95db0 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 19:51:09 +0100 Subject: [PATCH 08/31] refactor: S11.01 inline pool walks back into Create/Destroy The TryClaim / Release / SlotOwnsBase static-inline split was making the file harder to read, not easier. Reverting the extraction so both public functions hold the whole walk inline; intent is to factor it differently from a clean starting point. Behaviour is unchanged: per-iteration LockConfig/UnlockConfig in both walks, Initialise runs outside any lock, Cleanup stays inside its per-iteration critical section, Fallback short-circuits Destroy, unknown handles report a WARNING. Tests still pass (1127 ran, 0 failures); cppcheck-misra count stays at 87, no new suppressions. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 64 +++++-------------- 1 file changed, 17 insertions(+), 47 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 42fd50df..e03f893e 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -23,14 +23,6 @@ struct Slot static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead); static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); -static inline struct SolidSyslogBuffer* CircularBuffer_TryClaim( - struct SolidSyslogMutex* mutex, - uint8_t* ring, - size_t ringBytes -); -static inline bool CircularBuffer_Release(struct SolidSyslogBuffer* base); -static inline bool CircularBuffer_SlotOwnsBase(const struct Slot* slot, const struct SolidSyslogBuffer* base); - static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; static struct SolidSyslogBuffer Fallback = {Fallback_Write, Fallback_Read}; @@ -39,21 +31,6 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( uint8_t* ring, size_t ringBytes ) -{ - struct SolidSyslogBuffer* claimed = CircularBuffer_TryClaim(mutex, ring, ringBytes); - if (claimed == NULL) - { - SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED); - claimed = &Fallback; - } - return claimed; -} - -static inline struct SolidSyslogBuffer* CircularBuffer_TryClaim( - struct SolidSyslogMutex* mutex, - uint8_t* ring, - size_t ringBytes -) { size_t claimedIndex = SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; for (size_t i = 0; @@ -68,12 +45,16 @@ static inline struct SolidSyslogBuffer* CircularBuffer_TryClaim( } SolidSyslog_UnlockConfig(); } - struct SolidSyslogBuffer* claimed = NULL; + struct SolidSyslogBuffer* claimed = &Fallback; if (claimedIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) { CircularBuffer_Initialise(&Pool[claimedIndex].Object, mutex, ring, ringBytes); claimed = &Pool[claimedIndex].Object.Base; } + else + { + SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED); + } return claimed; } @@ -81,34 +62,23 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { if (base != &Fallback) { - bool released = CircularBuffer_Release(base); - if (!released) + bool released = false; + for (size_t i = 0; (i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && !released; i++) { - SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY); + SolidSyslog_LockConfig(); + if (Pool[i].InUse && (base == &Pool[i].Object.Base)) + { + CircularBuffer_Cleanup(base); + Pool[i].InUse = false; + released = true; + } + SolidSyslog_UnlockConfig(); } - } -} - -static inline bool CircularBuffer_Release(struct SolidSyslogBuffer* base) -{ - bool released = false; - for (size_t i = 0; (i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && !released; i++) - { - SolidSyslog_LockConfig(); - if (Pool[i].InUse && CircularBuffer_SlotOwnsBase(&Pool[i], base)) + if (!released) { - CircularBuffer_Cleanup(base); - Pool[i].InUse = false; - released = true; + SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY); } - SolidSyslog_UnlockConfig(); } - return released; -} - -static inline bool CircularBuffer_SlotOwnsBase(const struct Slot* slot, const struct SolidSyslogBuffer* base) -{ - return base == &slot->Object.Base; } static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead) From 170b1b116c4cb3a83ccc3a6067d75310de9d45bc Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 19:51:20 +0100 Subject: [PATCH 09/31] test: S11.01 lock-count tests + Pool-group diagnostics Pool group: - New MakeBuffer() fixture method removes the (mutex, ring, sizeof(ring)) repetition at every Create call site. - New CHECK_IS_FALLBACK(buf, pool) macro turns the bare-CHECK trio in FillingPoolThenOverflowReturnsDistinctFallback into three named CHECK_TEXT calls that say which constituent failed and why. - Two new tests pin the per-iteration locking property added in 4a34777 -- they assert LockCallCount == SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE for both Create-when-full and Destroy-of-last-issued. At default POOL_SIZE=1 these reduce to ONCE (the existing on-first-match tests already cover that); at POOL_SIZE > 1 they observe the per-iteration shape. - Existing CreateAcquiresAndReleasesConfigLock / Destroy... renamed to "...OnFirstFreeSlot" / "...OnFirstSlotMatch" to make their shape explicit alongside the new tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- Tests/SolidSyslogCircularBufferTest.cpp | 72 +++++++++++++++++++------ 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/Tests/SolidSyslogCircularBufferTest.cpp b/Tests/SolidSyslogCircularBufferTest.cpp index d9005c75..1b9ef2c1 100644 --- a/Tests/SolidSyslogCircularBufferTest.cpp +++ b/Tests/SolidSyslogCircularBufferTest.cpp @@ -16,6 +16,23 @@ using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-file scope only; brings ONCE/NEVER into scope for CALLED_FAKE +// Asserts buf is a non-null handle that is not one of the slots in pool. +// Used to pin the pool-exhaustion Fallback contract: every legitimate +// _Create returns either a pool slot or the Fallback singleton, never NULL. +// NOLINTBEGIN(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) -- macros preserve __FILE__/__LINE__ at the call site +#define CHECK_IS_FALLBACK(buf, pool) \ + do \ + { \ + CHECK_TEXT((buf) != nullptr, "Fallback handle was nullptr"); \ + for (auto* slot : (pool)) \ + { \ + CHECK_TEXT(slot != nullptr, "pool slot was nullptr (FillPool failed?)"); \ + CHECK_TEXT((buf) != slot, "Fallback handle collided with a pool slot"); \ + } \ + } while (0) + +// NOLINTEND(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) + enum { TEST_MAX_MESSAGES = 1 @@ -364,11 +381,16 @@ TEST_GROUP(SolidSyslogCircularBufferPool) ErrorHandlerFake_Uninstall(); } + struct SolidSyslogBuffer* MakeBuffer() + { + return SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + } + void FillPool() { for (auto*& slot : pooled) { - slot = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + slot = MakeBuffer(); } } }; @@ -378,14 +400,9 @@ TEST_GROUP(SolidSyslogCircularBufferPool) TEST(SolidSyslogCircularBufferPool, FillingPoolThenOverflowReturnsDistinctFallback) { FillPool(); - overflow = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + overflow = MakeBuffer(); - CHECK(overflow != nullptr); - for (auto* slot : pooled) - { - CHECK(slot != nullptr); - CHECK(overflow != slot); - } + CHECK_IS_FALLBACK(overflow, pooled); } TEST(SolidSyslogCircularBufferPool, ExhaustedCreateReportsError) @@ -393,7 +410,7 @@ TEST(SolidSyslogCircularBufferPool, ExhaustedCreateReportsError) ErrorHandlerFake_Install(nullptr); FillPool(); - overflow = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + overflow = MakeBuffer(); CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, ErrorHandlerFake_LastSeverity()); @@ -402,7 +419,7 @@ TEST(SolidSyslogCircularBufferPool, ExhaustedCreateReportsError) TEST(SolidSyslogCircularBufferPool, FallbackWriteAndReadAreNoOps) { FillPool(); - overflow = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + overflow = MakeBuffer(); SolidSyslogBuffer_Write(overflow, "hello", 5); @@ -412,27 +429,52 @@ TEST(SolidSyslogCircularBufferPool, FallbackWriteAndReadAreNoOps) LONGS_EQUAL(0, bytesRead); } -TEST(SolidSyslogCircularBufferPool, CreateAcquiresAndReleasesConfigLock) +TEST(SolidSyslogCircularBufferPool, CreateAcquiresAndReleasesConfigLockOnFirstFreeSlot) { ConfigLockFake_Install(); - pooled[0] = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + pooled[0] = MakeBuffer(); CALLED_FAKE(ConfigLockFake_Lock, ONCE); CALLED_FAKE(ConfigLockFake_Unlock, ONCE); } -TEST(SolidSyslogCircularBufferPool, DestroyAcquiresAndReleasesConfigLock) +TEST(SolidSyslogCircularBufferPool, CreateLocksOncePerSlotProbedWhenPoolIsFull) +{ + FillPool(); + ConfigLockFake_Install(); + + overflow = MakeBuffer(); + + LONGS_EQUAL(SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE, ConfigLockFake_LockCallCount()); + LONGS_EQUAL(SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE, ConfigLockFake_UnlockCallCount()); +} + +TEST(SolidSyslogCircularBufferPool, DestroyAcquiresAndReleasesConfigLockOnFirstSlotMatch) { - pooled[0] = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + pooled[0] = MakeBuffer(); ConfigLockFake_Install(); SolidSyslogCircularBuffer_Destroy(pooled[0]); + pooled[0] = nullptr; CALLED_FAKE(ConfigLockFake_Lock, ONCE); CALLED_FAKE(ConfigLockFake_Unlock, ONCE); } +TEST(SolidSyslogCircularBufferPool, DestroyLocksOncePerSlotProbedUntilMatch) +{ + FillPool(); + struct SolidSyslogBuffer* lastIssued = pooled[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE - 1U]; + ConfigLockFake_Install(); + + SolidSyslogCircularBuffer_Destroy(lastIssued); + pooled[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE - 1U] = nullptr; + + LONGS_EQUAL(SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE, ConfigLockFake_LockCallCount()); + LONGS_EQUAL(SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE, ConfigLockFake_UnlockCallCount()); +} + TEST(SolidSyslogCircularBufferPool, DestroyOfUnknownHandleReportsWarning) { ErrorHandlerFake_Install(nullptr); @@ -448,7 +490,7 @@ TEST(SolidSyslogCircularBufferPool, DestroyOfUnknownHandleReportsWarning) TEST(SolidSyslogCircularBufferPool, DestroyOfFallbackHandleIsSilent) { FillPool(); - overflow = SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); + overflow = MakeBuffer(); ErrorHandlerFake_Install(nullptr); SolidSyslogCircularBuffer_Destroy(overflow); From b077ed16e3a4c0a963747351b763acf708e743e4 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 20:07:19 +0100 Subject: [PATCH 10/31] refactor: S11.01 drive Create slot-walk by pointer not index Eliminates claimedIndex from _Create. The slot-walk now tracks a struct SolidSyslogBuffer* initialised to &Fallback; a successful claim swings it to the slot's base pointer, and the loop breaks on the first non-Fallback observation. The single pointer carries both "have we claimed" and "which handle do we return". To make this work without a parallel index, CircularBuffer_Initialise gains the same base-pointer signature that _Cleanup already has -- both vtable lifecycle hooks now take struct SolidSyslogBuffer* and recover the concrete struct via CircularBuffer_SelfFromBase. The existing 11.3 line-pinned suppression follows the cast one line down inside the updated Initialise body; no new suppression sites. The comparisons against &Fallback are repeated -- next step is probably an IsFallback(p) helper to wrap them. Behaviour unchanged; 1127 tests pass, cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBuffer.c | 3 ++- Core/Source/SolidSyslogCircularBufferPrivate.h | 2 +- Core/Source/SolidSyslogCircularBufferStatic.c | 18 +++++++++--------- misra_suppressions.txt | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBuffer.c b/Core/Source/SolidSyslogCircularBuffer.c index 42840f6e..d1fd1d99 100644 --- a/Core/Source/SolidSyslogCircularBuffer.c +++ b/Core/Source/SolidSyslogCircularBuffer.c @@ -33,12 +33,13 @@ static inline void CircularBuffer_LoadRecord(struct SolidSyslogCircularBuffer* s static inline size_t CircularBuffer_PeekRecordSize(const struct SolidSyslogCircularBuffer* self); void CircularBuffer_Initialise( - struct SolidSyslogCircularBuffer* self, + struct SolidSyslogBuffer* base, struct SolidSyslogMutex* mutex, uint8_t* ring, size_t ringBytes ) { + struct SolidSyslogCircularBuffer* self = CircularBuffer_SelfFromBase(base); self->Base.Read = CircularBuffer_Read; self->Base.Write = CircularBuffer_Write; self->Mutex = mutex; diff --git a/Core/Source/SolidSyslogCircularBufferPrivate.h b/Core/Source/SolidSyslogCircularBufferPrivate.h index 7ab378c9..e4b59b6f 100644 --- a/Core/Source/SolidSyslogCircularBufferPrivate.h +++ b/Core/Source/SolidSyslogCircularBufferPrivate.h @@ -20,7 +20,7 @@ struct SolidSyslogCircularBuffer }; void CircularBuffer_Initialise( - struct SolidSyslogCircularBuffer* self, + struct SolidSyslogBuffer* base, struct SolidSyslogMutex* mutex, uint8_t* ring, size_t ringBytes diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index e03f893e..e34b4a1b 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -32,24 +32,24 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( size_t ringBytes ) { - size_t claimedIndex = SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; - for (size_t i = 0; - (i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && (claimedIndex == SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE); - i++) + struct SolidSyslogBuffer* claimed = &Fallback; + for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) { SolidSyslog_LockConfig(); if (!Pool[i].InUse) { Pool[i].InUse = true; - claimedIndex = i; + claimed = &Pool[i].Object.Base; } SolidSyslog_UnlockConfig(); + if (claimed != &Fallback) + { + break; + } } - struct SolidSyslogBuffer* claimed = &Fallback; - if (claimedIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) + if (claimed != &Fallback) { - CircularBuffer_Initialise(&Pool[claimedIndex].Object, mutex, ring, ringBytes); - claimed = &Pool[claimedIndex].Object.Base; + CircularBuffer_Initialise(claimed, mutex, ring, ringBytes); } else { diff --git a/misra_suppressions.txt b/misra_suppressions.txt index 2cd58ed0..863a18ed 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -24,7 +24,7 @@ misra-c2012-11.2:Platform/Windows/Source/SolidSyslogAddressInternal.h:23 misra-c2012-11.3:Core/Interface/SolidSyslogFormatter.h:30 misra-c2012-11.3:Core/Source/SolidSyslogBlockStore.c:82 misra-c2012-11.3:Core/Source/SolidSyslogBlockStore.c:165 -misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:90 +misra-c2012-11.3:Core/Source/SolidSyslogCircularBuffer.c:91 misra-c2012-11.3:Core/Source/SolidSyslogFileBlockDevice.c:118 misra-c2012-11.3:Core/Source/SolidSyslogFileBlockDevice.c:146 misra-c2012-11.3:Core/Source/SolidSyslogFormatter.c:93 From 69a03bfc863c242798424ae241491083ba0551c4 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 21:29:50 +0100 Subject: [PATCH 11/31] refactor: S11.01 extract CircularBuffer_TryClaimSlot(i) Lifts the lock/check/claim/unlock sequence into a per-slot helper. TryClaimSlot returns the slot's base pointer when the slot was free and is now claimed, or &Fallback when the slot was already in use. _Create's loop body collapses to a single assignment and the break. The handle returned is exactly what _Create wants to either pass to Initialise or hand back to the caller, so no second walk or index recovery is needed. Sits immediately above _Create -- defined before first use, no forward declaration. 1127 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index e34b4a1b..393f11ca 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -26,6 +26,19 @@ static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, siz static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; static struct SolidSyslogBuffer Fallback = {Fallback_Write, Fallback_Read}; +static struct SolidSyslogBuffer* CircularBuffer_TryClaimSlot(size_t i) +{ + struct SolidSyslogBuffer* claimed = &Fallback; + SolidSyslog_LockConfig(); + if (!Pool[i].InUse) + { + Pool[i].InUse = true; + claimed = &Pool[i].Object.Base; + } + SolidSyslog_UnlockConfig(); + return claimed; +} + struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( struct SolidSyslogMutex* mutex, uint8_t* ring, @@ -35,13 +48,7 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( struct SolidSyslogBuffer* claimed = &Fallback; for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) { - SolidSyslog_LockConfig(); - if (!Pool[i].InUse) - { - Pool[i].InUse = true; - claimed = &Pool[i].Object.Base; - } - SolidSyslog_UnlockConfig(); + claimed = CircularBuffer_TryClaimSlot(i); if (claimed != &Fallback) { break; From bc3e4639a6e165e910beec638024b44c1d63bf5f Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 21:39:46 +0100 Subject: [PATCH 12/31] refactor: S11.01 rename TryClaimSlot -> AcquireIfFree, claimed -> handle The "Try" in TryClaimSlot was misleading -- the function never fails, it always returns a usable handle, just sometimes the Fallback. "AcquireIfFree" reads honestly: it acquires the entry if free, otherwise hands back the Fallback. Drops the "Slot" terminology that was leaking the file-internal struct Slot name into the helper. The local "claimed" similarly read past-tense; the variable starts as &Fallback, which is the un-claimed state. "handle" matches the term the public API already uses for the buffer pointers it issues, and reads cleanly at both call sites: handle = &Fallback; (haven't found anything yet) handle = CircularBuffer_AcquireIfFree(i); 1127 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 393f11ca..6991ed2f 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -26,17 +26,17 @@ static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, siz static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; static struct SolidSyslogBuffer Fallback = {Fallback_Write, Fallback_Read}; -static struct SolidSyslogBuffer* CircularBuffer_TryClaimSlot(size_t i) +static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t i) { - struct SolidSyslogBuffer* claimed = &Fallback; + struct SolidSyslogBuffer* handle = &Fallback; SolidSyslog_LockConfig(); if (!Pool[i].InUse) { Pool[i].InUse = true; - claimed = &Pool[i].Object.Base; + handle = &Pool[i].Object.Base; } SolidSyslog_UnlockConfig(); - return claimed; + return handle; } struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( @@ -45,24 +45,24 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( size_t ringBytes ) { - struct SolidSyslogBuffer* claimed = &Fallback; + struct SolidSyslogBuffer* handle = &Fallback; for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) { - claimed = CircularBuffer_TryClaimSlot(i); - if (claimed != &Fallback) + handle = CircularBuffer_AcquireIfFree(i); + if (handle != &Fallback) { break; } } - if (claimed != &Fallback) + if (handle != &Fallback) { - CircularBuffer_Initialise(claimed, mutex, ring, ringBytes); + CircularBuffer_Initialise(handle, mutex, ring, ringBytes); } else { SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED); } - return claimed; + return handle; } void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) From db2d1665340ad0bb03f5bb775ea02d9317bcdb74 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 21:41:22 +0100 Subject: [PATCH 13/31] refactor: S11.01 maintain function order in CircularBufferStatic Per CLAUDE.md "Function Ordering": _Create first, _Destroy second, helpers defined directly below their first caller with forward declarations grouped at the top. CircularBuffer_AcquireIfFree moves from above _Create to immediately below it, joined by a forward declaration alongside the Fallback_Read / Fallback_Write decls. No behaviour change; 1127 tests pass, cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 6991ed2f..0cb14c74 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -22,23 +22,11 @@ struct Slot static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead); static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); +static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t i); static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; static struct SolidSyslogBuffer Fallback = {Fallback_Write, Fallback_Read}; -static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t i) -{ - struct SolidSyslogBuffer* handle = &Fallback; - SolidSyslog_LockConfig(); - if (!Pool[i].InUse) - { - Pool[i].InUse = true; - handle = &Pool[i].Object.Base; - } - SolidSyslog_UnlockConfig(); - return handle; -} - struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( struct SolidSyslogMutex* mutex, uint8_t* ring, @@ -65,6 +53,19 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( return handle; } +static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t i) +{ + struct SolidSyslogBuffer* handle = &Fallback; + SolidSyslog_LockConfig(); + if (!Pool[i].InUse) + { + Pool[i].InUse = true; + handle = &Pool[i].Object.Base; + } + SolidSyslog_UnlockConfig(); + return handle; +} + void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { if (base != &Fallback) From 4eaf456d35251632752ee56631050690ae6c49f8 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 21:45:39 +0100 Subject: [PATCH 14/31] refactor: S11.01 extract CircularBuffer_HandleIsValid The three "handle != &Fallback" / "base != &Fallback" sites in _Create and _Destroy now read as a named predicate. The Fallback sentinel address is now mentioned in exactly one place in the file -- the helper body -- so a future change of the sentinel mechanism (e.g. a per-class null-object table or a different signalling strategy) is a one-line edit. Skipping the symmetric HandleIsInvalid for now: no production site uses the negative form, and tests don't see &Fallback today. Easy follow-up when a test (or a future production guard-clause) wants positive-logic against "this is the fallback". 1127 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 0cb14c74..51a83599 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -23,6 +23,7 @@ struct Slot static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead); static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t i); +static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; static struct SolidSyslogBuffer Fallback = {Fallback_Write, Fallback_Read}; @@ -37,12 +38,12 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) { handle = CircularBuffer_AcquireIfFree(i); - if (handle != &Fallback) + if (CircularBuffer_HandleIsValid(handle)) { break; } } - if (handle != &Fallback) + if (CircularBuffer_HandleIsValid(handle)) { CircularBuffer_Initialise(handle, mutex, ring, ringBytes); } @@ -66,9 +67,14 @@ static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t i) return handle; } +static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle) +{ + return handle != &Fallback; +} + void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { - if (base != &Fallback) + if (CircularBuffer_HandleIsValid(base)) { bool released = false; for (size_t i = 0; (i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && !released; i++) From 571760c814ed99c0adea93e2f265edfd3dbeb529 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 21:48:13 +0100 Subject: [PATCH 15/31] refactor: S11.01 extract CircularBuffer_AcquireFirstFree _Create now reads top-to-bottom as: handle = AcquireFirstFree(); if HandleIsValid -> Initialise else -> Error return handle; The pool walk lives one level down in AcquireFirstFree, where the loop bound and the per-iteration AcquireIfFree call are the only shape. _Create itself is six executable lines of pure intent. Function order: AcquireFirstFree is defined directly below _Create; AcquireIfFree directly below AcquireFirstFree (its first caller); HandleIsValid directly below that. _Destroy follows. Naming kept parallel with AcquireIfFree -- both are verb + free- condition with no "Handle" suffix; the `handle` variable at the call site documents the return shape. Trivial to bulk-rename both to *Handle later if the file gets more such helpers. 1127 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 51a83599..59875c60 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -22,6 +22,7 @@ struct Slot static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead); static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); +static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void); static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t i); static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); @@ -33,6 +34,20 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( uint8_t* ring, size_t ringBytes ) +{ + struct SolidSyslogBuffer* handle = CircularBuffer_AcquireFirstFree(); + if (CircularBuffer_HandleIsValid(handle)) + { + CircularBuffer_Initialise(handle, mutex, ring, ringBytes); + } + else + { + SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED); + } + return handle; +} + +static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void) { struct SolidSyslogBuffer* handle = &Fallback; for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) @@ -43,14 +58,6 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( break; } } - if (CircularBuffer_HandleIsValid(handle)) - { - CircularBuffer_Initialise(handle, mutex, ring, ringBytes); - } - else - { - SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_POOL_EXHAUSTED); - } return handle; } From c881c08f2870aed81665ac0ddd24db643b38b35e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 21:50:35 +0100 Subject: [PATCH 16/31] refactor: S11.01 rename loop/parameter i to poolIndex Pool[poolIndex].InUse reads as a unit at every site; the name ties the index to the only collection it can index in the file, matching FileBlockDevice.c's blockIndex pattern. Applied to AcquireIfFree's parameter, AcquireFirstFree's loop, and _Destroy's loop -- all three are indices into Pool. 1127 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 59875c60..89bb0e51 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -23,7 +23,7 @@ struct Slot static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead); static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void); -static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t i); +static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex); static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; @@ -50,9 +50,9 @@ struct SolidSyslogBuffer* SolidSyslogCircularBuffer_Create( static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void) { struct SolidSyslogBuffer* handle = &Fallback; - for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) + for (size_t poolIndex = 0; poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; poolIndex++) { - handle = CircularBuffer_AcquireIfFree(i); + handle = CircularBuffer_AcquireIfFree(poolIndex); if (CircularBuffer_HandleIsValid(handle)) { break; @@ -61,14 +61,14 @@ static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void) return handle; } -static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t i) +static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex) { struct SolidSyslogBuffer* handle = &Fallback; SolidSyslog_LockConfig(); - if (!Pool[i].InUse) + if (!Pool[poolIndex].InUse) { - Pool[i].InUse = true; - handle = &Pool[i].Object.Base; + Pool[poolIndex].InUse = true; + handle = &Pool[poolIndex].Object.Base; } SolidSyslog_UnlockConfig(); return handle; @@ -84,13 +84,13 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) if (CircularBuffer_HandleIsValid(base)) { bool released = false; - for (size_t i = 0; (i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && !released; i++) + for (size_t poolIndex = 0; (poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && !released; poolIndex++) { SolidSyslog_LockConfig(); - if (Pool[i].InUse && (base == &Pool[i].Object.Base)) + if (Pool[poolIndex].InUse && (base == &Pool[poolIndex].Object.Base)) { CircularBuffer_Cleanup(base); - Pool[i].InUse = false; + Pool[poolIndex].InUse = false; released = true; } SolidSyslog_UnlockConfig(); From 5d0614034bb2309179a4c552852430c7fe1794ff Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 21:54:34 +0100 Subject: [PATCH 17/31] refactor: S11.01 extract CircularBuffer_PoolItemIsFree AcquireIfFree's guard now reads as a near-quote of the function name: "if PoolItemIsFree, claim it". The !InUse negation is hidden behind the predicate, leaving the function body in pure positive logic. Left _Destroy's "Pool[poolIndex].InUse && ..." check alone -- it wants the positive in-use sense, and PoolItemIsFree's inverse would be negative logic at the call site. If the symmetry matters later, PoolItemIsInUse is the natural pair to add. 1127 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 89bb0e51..271c27c1 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -24,6 +24,7 @@ static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t max static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, size_t size); static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void); static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex); +static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex); static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; @@ -65,7 +66,7 @@ static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex) { struct SolidSyslogBuffer* handle = &Fallback; SolidSyslog_LockConfig(); - if (!Pool[poolIndex].InUse) + if (CircularBuffer_PoolItemIsFree(poolIndex)) { Pool[poolIndex].InUse = true; handle = &Pool[poolIndex].Object.Base; @@ -74,6 +75,11 @@ static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex) return handle; } +static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex) +{ + return !Pool[poolIndex].InUse; +} + static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle) { return handle != &Fallback; From 142571f258729e1b28bf6ca2baba1fc00bdcdef8 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 21:55:48 +0100 Subject: [PATCH 18/31] refactor: S11.01 extract CircularBuffer_Acquire The two lines that actually do the claim -- mark the pool item in-use and hand back its base pointer -- now live in a dedicated Acquire helper. The vocabulary nests: AcquireFirstFree -> walk pool, return first acquired AcquireIfFree -> probe one slot, lock-check-Acquire-unlock Acquire -> unconditional mark-and-return for one slot AcquireIfFree's body now reads as a sentence: if PoolItemIsFree, Acquire it. 1127 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 271c27c1..b8ad5a72 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -25,6 +25,7 @@ static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, siz static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void); static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex); static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex); +static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex); static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; @@ -68,8 +69,7 @@ static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex) SolidSyslog_LockConfig(); if (CircularBuffer_PoolItemIsFree(poolIndex)) { - Pool[poolIndex].InUse = true; - handle = &Pool[poolIndex].Object.Base; + handle = CircularBuffer_Acquire(poolIndex); } SolidSyslog_UnlockConfig(); return handle; @@ -80,6 +80,12 @@ static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex) return !Pool[poolIndex].InUse; } +static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex) +{ + Pool[poolIndex].InUse = true; + return &Pool[poolIndex].Object.Base; +} + static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle) { return handle != &Fallback; From 650044019f0a1ebac7e9712e9136ef5a6fb4c478 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 22:03:51 +0100 Subject: [PATCH 19/31] feat: S11.01 drop Fallback special-case from _Destroy _Destroy no longer treats &Fallback as a privileged input. The Fallback singleton is never installed in Pool, so the slot-walk simply doesn't match it and falls through to the existing WARNING path. The contract changes from Destroy(&Fallback) -> silent Destroy(other unknown handle) -> WARNING to a single uniform rule: Destroy(handle not currently issued by Create) -> WARNING regardless of whether that handle was once issued by Create (pool-exhaustion Fallback) or never was (stack-fabricated stranger, double-Destroy, etc). The function body drops one indent level and the asymmetric "we silently swallow the Fallback we issued you" special case. The existing DestroyOfUnknownHandleReportsWarning test already covers the WARNING path; the now-incorrect DestroyOfFallbackHandleIsSilent test is deleted (its assertion no longer holds). HandleIsValid stays -- still used by _Create and AcquireFirstFree. 1126 tests pass (-1 from the deletion); cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 27 +++++++++---------- Tests/SolidSyslogCircularBufferTest.cpp | 12 --------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index b8ad5a72..8cf61773 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -93,24 +93,21 @@ static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { - if (CircularBuffer_HandleIsValid(base)) + bool released = false; + for (size_t poolIndex = 0; (poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && !released; poolIndex++) { - bool released = false; - for (size_t poolIndex = 0; (poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && !released; poolIndex++) + SolidSyslog_LockConfig(); + if (Pool[poolIndex].InUse && (base == &Pool[poolIndex].Object.Base)) { - SolidSyslog_LockConfig(); - if (Pool[poolIndex].InUse && (base == &Pool[poolIndex].Object.Base)) - { - CircularBuffer_Cleanup(base); - Pool[poolIndex].InUse = false; - released = true; - } - SolidSyslog_UnlockConfig(); - } - if (!released) - { - SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY); + CircularBuffer_Cleanup(base); + Pool[poolIndex].InUse = false; + released = true; } + SolidSyslog_UnlockConfig(); + } + if (!released) + { + SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_WARNING, SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY); } } diff --git a/Tests/SolidSyslogCircularBufferTest.cpp b/Tests/SolidSyslogCircularBufferTest.cpp index 1b9ef2c1..da4cdc8a 100644 --- a/Tests/SolidSyslogCircularBufferTest.cpp +++ b/Tests/SolidSyslogCircularBufferTest.cpp @@ -486,15 +486,3 @@ TEST(SolidSyslogCircularBufferPool, DestroyOfUnknownHandleReportsWarning) LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity()); STRCMP_EQUAL(SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY, ErrorHandlerFake_LastMessage()); } - -TEST(SolidSyslogCircularBufferPool, DestroyOfFallbackHandleIsSilent) -{ - FillPool(); - overflow = MakeBuffer(); - ErrorHandlerFake_Install(nullptr); - - SolidSyslogCircularBuffer_Destroy(overflow); - overflow = nullptr; - - CALLED_FAKE(ErrorHandlerFake_Handle, NEVER); -} From a396e1c677febec127d9a29981b6bc9a113f688b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 22:13:41 +0100 Subject: [PATCH 20/31] refactor: S11.01 _Destroy splits scan-then-release, drops per-probe lock _Destroy now has two phases at the top level: 1. Find the matching poolIndex by scanning constant slot addresses (no lock needed -- &Pool[i].Object.Base never moves). 2. If a match was found, acquire the lock once, check InUse, conditionally Cleanup + free + release. Single lock acquisition per Destroy at most. Lock-count contract changes accordingly: - Pool-issued, currently-in-use handle: 1 lock (was 1..N). - Pool-issued, already-destroyed handle: 1 lock (was 1..N). - Unknown handle (no address match): 0 locks (was N). Externally-visible behaviour for the integrator is unchanged: live handles release cleanly, stale or stranger handles get the WARNING. The "stale-pool-address" case (double-Destroy or use-after-Destroy) already went down the WARNING path before -- the compound condition in the old loop just failed -- so this isn't a new failure mode, just made explicit. Tests updated: - DestroyAcquiresAndReleasesConfigLockOnFirstSlotMatch -> DestroyOfPooledHandleLocksOnce. The "OnFirstSlotMatch" qualifier is now obsolete; any pool-issued handle locks exactly once. - DestroyLocksOncePerSlotProbedUntilMatch (N locks) replaced by DestroyOfUnknownHandleDoesNotLock (NEVER). The N-locks property no longer holds and the zero-locks property is new and worth pinning. 1126 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 13 +++++++++++-- Tests/SolidSyslogCircularBufferTest.cpp | 14 ++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 8cf61773..611e387b 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -93,11 +93,20 @@ static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { + size_t poolIndex = SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; + for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) + { + if (base == &Pool[i].Object.Base) + { + poolIndex = i; + break; + } + } bool released = false; - for (size_t poolIndex = 0; (poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) && !released; poolIndex++) + if (poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) { SolidSyslog_LockConfig(); - if (Pool[poolIndex].InUse && (base == &Pool[poolIndex].Object.Base)) + if (Pool[poolIndex].InUse) { CircularBuffer_Cleanup(base); Pool[poolIndex].InUse = false; diff --git a/Tests/SolidSyslogCircularBufferTest.cpp b/Tests/SolidSyslogCircularBufferTest.cpp index da4cdc8a..f1330319 100644 --- a/Tests/SolidSyslogCircularBufferTest.cpp +++ b/Tests/SolidSyslogCircularBufferTest.cpp @@ -450,7 +450,7 @@ TEST(SolidSyslogCircularBufferPool, CreateLocksOncePerSlotProbedWhenPoolIsFull) LONGS_EQUAL(SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE, ConfigLockFake_UnlockCallCount()); } -TEST(SolidSyslogCircularBufferPool, DestroyAcquiresAndReleasesConfigLockOnFirstSlotMatch) +TEST(SolidSyslogCircularBufferPool, DestroyOfPooledHandleLocksOnce) { pooled[0] = MakeBuffer(); ConfigLockFake_Install(); @@ -462,17 +462,15 @@ TEST(SolidSyslogCircularBufferPool, DestroyAcquiresAndReleasesConfigLockOnFirstS CALLED_FAKE(ConfigLockFake_Unlock, ONCE); } -TEST(SolidSyslogCircularBufferPool, DestroyLocksOncePerSlotProbedUntilMatch) +TEST(SolidSyslogCircularBufferPool, DestroyOfUnknownHandleDoesNotLock) { - FillPool(); - struct SolidSyslogBuffer* lastIssued = pooled[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE - 1U]; ConfigLockFake_Install(); + struct SolidSyslogBuffer stranger = {}; - SolidSyslogCircularBuffer_Destroy(lastIssued); - pooled[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE - 1U] = nullptr; + SolidSyslogCircularBuffer_Destroy(&stranger); - LONGS_EQUAL(SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE, ConfigLockFake_LockCallCount()); - LONGS_EQUAL(SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE, ConfigLockFake_UnlockCallCount()); + CALLED_FAKE(ConfigLockFake_Lock, NEVER); + CALLED_FAKE(ConfigLockFake_Unlock, NEVER); } TEST(SolidSyslogCircularBufferPool, DestroyOfUnknownHandleReportsWarning) From e478b747d090bd959255294886df10f73ddde308 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 22:19:12 +0100 Subject: [PATCH 21/31] refactor: S11.01 extract CircularBuffer_HandleFromIndex &Pool[poolIndex].Object.Base appeared at two unrelated sites -- Acquire's return and _Destroy's address-match scan -- with the same "convert pool index to the handle the integrator sees" meaning. Lifted into HandleFromIndex(poolIndex) so each site reads as intent, not field navigation. 1126 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 611e387b..2204dd76 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -26,6 +26,7 @@ static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void); static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex); static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex); static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex); +static inline struct SolidSyslogBuffer* CircularBuffer_HandleFromIndex(size_t poolIndex); static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; @@ -83,6 +84,11 @@ static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex) static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex) { Pool[poolIndex].InUse = true; + return CircularBuffer_HandleFromIndex(poolIndex); +} + +static inline struct SolidSyslogBuffer* CircularBuffer_HandleFromIndex(size_t poolIndex) +{ return &Pool[poolIndex].Object.Base; } @@ -96,7 +102,7 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) size_t poolIndex = SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) { - if (base == &Pool[i].Object.Base) + if (base == CircularBuffer_HandleFromIndex(i)) { poolIndex = i; break; From ed0dbdd88b454b85ab73c6090b586557def71f24 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 22:23:57 +0100 Subject: [PATCH 22/31] refactor: S11.01 extract MarkInUse / MarkFree pair Pool[poolIndex].InUse mutations now go through named verbs -- MarkInUse(poolIndex) from Acquire, MarkFree(poolIndex) from _Destroy's release block. The field-write is hidden the same way PoolItemIsFree hides the field-read, so the file reads as a consistent vocabulary at one level of abstraction. Pair-of-helpers, not single-helper-with-bool-arg, so call sites don't read "set in-use to false" and instead read "mark free" -- intent at the verb, no boolean to translate. 1126 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 2204dd76..481366d3 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -26,8 +26,10 @@ static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void); static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex); static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex); static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex); +static inline void CircularBuffer_MarkInUse(size_t poolIndex); static inline struct SolidSyslogBuffer* CircularBuffer_HandleFromIndex(size_t poolIndex); static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); +static inline void CircularBuffer_MarkFree(size_t poolIndex); static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; static struct SolidSyslogBuffer Fallback = {Fallback_Write, Fallback_Read}; @@ -83,10 +85,15 @@ static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex) static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex) { - Pool[poolIndex].InUse = true; + CircularBuffer_MarkInUse(poolIndex); return CircularBuffer_HandleFromIndex(poolIndex); } +static inline void CircularBuffer_MarkInUse(size_t poolIndex) +{ + Pool[poolIndex].InUse = true; +} + static inline struct SolidSyslogBuffer* CircularBuffer_HandleFromIndex(size_t poolIndex) { return &Pool[poolIndex].Object.Base; @@ -115,7 +122,7 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) if (Pool[poolIndex].InUse) { CircularBuffer_Cleanup(base); - Pool[poolIndex].InUse = false; + CircularBuffer_MarkFree(poolIndex); released = true; } SolidSyslog_UnlockConfig(); @@ -126,6 +133,11 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) } } +static inline void CircularBuffer_MarkFree(size_t poolIndex) +{ + Pool[poolIndex].InUse = false; +} + static bool Fallback_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead) { (void) base; From 0da53182325997c8ba6578c98ffcb906f0133e0b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 22:26:33 +0100 Subject: [PATCH 23/31] refactor: S11.01 extract CircularBuffer_IndexFromHandle _Destroy's head reduces to one line of intent: poolIndex = IndexFromHandle(base); The find loop now lives in its own function -- the natural inverse of HandleFromIndex(i). Together the pair forms a closed conversion: HandleFromIndex(i) -> base pointer for slot i IndexFromHandle(base) -> slot index for that pointer, or POOL_SIZE if base wasn't issued _Destroy now reads at one level of abstraction: get the index, release-if-in-use under lock, warn-if-not-released. 1126 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 481366d3..7f6b1eae 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -29,6 +29,7 @@ static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex) static inline void CircularBuffer_MarkInUse(size_t poolIndex); static inline struct SolidSyslogBuffer* CircularBuffer_HandleFromIndex(size_t poolIndex); static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); +static size_t CircularBuffer_IndexFromHandle(const struct SolidSyslogBuffer* base); static inline void CircularBuffer_MarkFree(size_t poolIndex); static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; @@ -106,15 +107,7 @@ static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { - size_t poolIndex = SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; - for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) - { - if (base == CircularBuffer_HandleFromIndex(i)) - { - poolIndex = i; - break; - } - } + size_t poolIndex = CircularBuffer_IndexFromHandle(base); bool released = false; if (poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) { @@ -133,6 +126,20 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) } } +static size_t CircularBuffer_IndexFromHandle(const struct SolidSyslogBuffer* base) +{ + size_t poolIndex = SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; + for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) + { + if (base == CircularBuffer_HandleFromIndex(i)) + { + poolIndex = i; + break; + } + } + return poolIndex; +} + static inline void CircularBuffer_MarkFree(size_t poolIndex) { Pool[poolIndex].InUse = false; From 8f5f89f700f65fe56a2cb92d8bf0d977da6cf105 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 22:28:18 +0100 Subject: [PATCH 24/31] refactor: S11.01 extract CircularBuffer_PoolIndexIsValid The "poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE" check was shorthand for "IndexFromHandle returned a real index, not the sentinel". Naming it makes the condition self-documenting: if (CircularBuffer_PoolIndexIsValid(poolIndex)) Subject-IS-adjective form matches the other predicates in the file (PoolItemIsFree, HandleIsValid). 1126 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 7f6b1eae..f93233c3 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -30,6 +30,7 @@ static inline void CircularBuffer_MarkInUse(size_t poolIndex); static inline struct SolidSyslogBuffer* CircularBuffer_HandleFromIndex(size_t poolIndex); static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); static size_t CircularBuffer_IndexFromHandle(const struct SolidSyslogBuffer* base); +static inline bool CircularBuffer_PoolIndexIsValid(size_t poolIndex); static inline void CircularBuffer_MarkFree(size_t poolIndex); static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; @@ -109,7 +110,7 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) { size_t poolIndex = CircularBuffer_IndexFromHandle(base); bool released = false; - if (poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE) + if (CircularBuffer_PoolIndexIsValid(poolIndex)) { SolidSyslog_LockConfig(); if (Pool[poolIndex].InUse) @@ -140,6 +141,11 @@ static size_t CircularBuffer_IndexFromHandle(const struct SolidSyslogBuffer* bas return poolIndex; } +static inline bool CircularBuffer_PoolIndexIsValid(size_t poolIndex) +{ + return poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; +} + static inline void CircularBuffer_MarkFree(size_t poolIndex) { Pool[poolIndex].InUse = false; From 9712f678baf6264e42e6789a53a74444c3f61c6b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 22:32:28 +0100 Subject: [PATCH 25/31] refactor: S11.01 chain PoolItemIsFree through PoolItemIsInUse Raw Pool[poolIndex].InUse reads collapse to a single site. The field's full surface in the file is now: PoolItemIsInUse(poolIndex) -- the only read MarkInUse(poolIndex) -- the only set-to-true MarkFree(poolIndex) -- the only set-to-false Everything else expresses intent. PoolItemIsFree becomes the negation of PoolItemIsInUse, so a future representation change (atomic flag, packed bitset, separate free-list, etc) touches just the three accessor functions. 1126 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index f93233c3..e28f6476 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -25,6 +25,7 @@ static void Fallback_Write(struct SolidSyslogBuffer* base, const void* data, siz static struct SolidSyslogBuffer* CircularBuffer_AcquireFirstFree(void); static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex); static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex); +static inline bool CircularBuffer_PoolItemIsInUse(size_t poolIndex); static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex); static inline void CircularBuffer_MarkInUse(size_t poolIndex); static inline struct SolidSyslogBuffer* CircularBuffer_HandleFromIndex(size_t poolIndex); @@ -82,7 +83,12 @@ static struct SolidSyslogBuffer* CircularBuffer_AcquireIfFree(size_t poolIndex) static inline bool CircularBuffer_PoolItemIsFree(size_t poolIndex) { - return !Pool[poolIndex].InUse; + return !CircularBuffer_PoolItemIsInUse(poolIndex); +} + +static inline bool CircularBuffer_PoolItemIsInUse(size_t poolIndex) +{ + return Pool[poolIndex].InUse; } static inline struct SolidSyslogBuffer* CircularBuffer_Acquire(size_t poolIndex) @@ -113,7 +119,7 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) if (CircularBuffer_PoolIndexIsValid(poolIndex)) { SolidSyslog_LockConfig(); - if (Pool[poolIndex].InUse) + if (CircularBuffer_PoolItemIsInUse(poolIndex)) { CircularBuffer_Cleanup(base); CircularBuffer_MarkFree(poolIndex); From d671f52207cf0a320bd06a94367245455c4c0dc9 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 22:35:05 +0100 Subject: [PATCH 26/31] refactor: S11.01 extract CircularBuffer_FreeIfInUse Symmetric counterpart to AcquireIfFree: lock, check IsInUse, do the work (Cleanup + MarkFree), unlock, return whether anything was freed. _Destroy collapses to the lifecycle skeleton: poolIndex = IndexFromHandle(base); if PoolIndexIsValid -> released = FreeIfInUse(poolIndex); if not released -> warn; FreeIfInUse takes only the poolIndex, mirroring AcquireIfFree's single-arg signature. The handle for Cleanup is recomputed internally via HandleFromIndex -- no risk of a base argument that diverges from the index. 1126 tests pass; cppcheck-misra stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index e28f6476..471d3729 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -32,6 +32,7 @@ static inline struct SolidSyslogBuffer* CircularBuffer_HandleFromIndex(size_t po static inline bool CircularBuffer_HandleIsValid(const struct SolidSyslogBuffer* handle); static size_t CircularBuffer_IndexFromHandle(const struct SolidSyslogBuffer* base); static inline bool CircularBuffer_PoolIndexIsValid(size_t poolIndex); +static bool CircularBuffer_FreeIfInUse(size_t poolIndex); static inline void CircularBuffer_MarkFree(size_t poolIndex); static struct Slot Pool[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE]; @@ -118,14 +119,7 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) bool released = false; if (CircularBuffer_PoolIndexIsValid(poolIndex)) { - SolidSyslog_LockConfig(); - if (CircularBuffer_PoolItemIsInUse(poolIndex)) - { - CircularBuffer_Cleanup(base); - CircularBuffer_MarkFree(poolIndex); - released = true; - } - SolidSyslog_UnlockConfig(); + released = CircularBuffer_FreeIfInUse(poolIndex); } if (!released) { @@ -152,6 +146,20 @@ static inline bool CircularBuffer_PoolIndexIsValid(size_t poolIndex) return poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; } +static bool CircularBuffer_FreeIfInUse(size_t poolIndex) +{ + bool released = false; + SolidSyslog_LockConfig(); + if (CircularBuffer_PoolItemIsInUse(poolIndex)) + { + CircularBuffer_Cleanup(CircularBuffer_HandleFromIndex(poolIndex)); + CircularBuffer_MarkFree(poolIndex); + released = true; + } + SolidSyslog_UnlockConfig(); + return released; +} + static inline void CircularBuffer_MarkFree(size_t poolIndex) { Pool[poolIndex].InUse = false; From 10c6b1ba4b628a20f64a2b34a085cba223313ef8 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 22:39:18 +0100 Subject: [PATCH 27/31] refactor: S11.01 IndexFromHandle uses poolIndex as the loop var The loop now iterates poolIndex consistently with AcquireFirstFree; the function's return becomes `result`, naming its role rather than its type. The previous shape used the inconsistent `i` to avoid shadowing -- this is the cleaner resolution. Final state of CircularBufferStatic.c on this branch: - Public API: _Create and _Destroy each read as 4-6 lines of pure intent against named helpers. - Helpers organised three levels deep: * Composition -- AcquireFirstFree, AcquireIfFree, IndexFromHandle, FreeIfInUse, Acquire * Predicates -- HandleIsValid, PoolIndexIsValid, PoolItemIsFree (-> chains through PoolItemIsInUse) * Atomic ops -- MarkInUse, MarkFree, HandleFromIndex, PoolItemIsInUse - Pool[poolIndex].InUse touched in exactly three places: one read (PoolItemIsInUse), one set-true (MarkInUse), one set-false (MarkFree). - &Fallback referenced in exactly two places: the static initialiser and HandleIsValid. - &Pool[poolIndex].Object.Base computed in exactly one place (HandleFromIndex). - Zero MISRA warnings in this TU; zero new cppcheck suppressions or NOLINT comments anywhere on the branch since main. - Lock duration: per-iteration around Acquire (so Initialise runs unlocked); single acquire on _Destroy only when a pool address matches. - 1126 tests pass; cppcheck-misra count 87 (was 88 on main). This is the reference shape for the rest of E11. Co-Authored-By: Claude Opus 4.7 (1M context) --- Core/Source/SolidSyslogCircularBufferStatic.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Core/Source/SolidSyslogCircularBufferStatic.c b/Core/Source/SolidSyslogCircularBufferStatic.c index 471d3729..836e274a 100644 --- a/Core/Source/SolidSyslogCircularBufferStatic.c +++ b/Core/Source/SolidSyslogCircularBufferStatic.c @@ -129,16 +129,16 @@ void SolidSyslogCircularBuffer_Destroy(struct SolidSyslogBuffer* base) static size_t CircularBuffer_IndexFromHandle(const struct SolidSyslogBuffer* base) { - size_t poolIndex = SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; - for (size_t i = 0; i < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; i++) + size_t result = SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; + for (size_t poolIndex = 0; poolIndex < SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE; poolIndex++) { - if (base == CircularBuffer_HandleFromIndex(i)) + if (base == CircularBuffer_HandleFromIndex(poolIndex)) { - poolIndex = i; + result = poolIndex; break; } } - return poolIndex; + return result; } static inline bool CircularBuffer_PoolIndexIsValid(size_t poolIndex) From 539d9a2b2489bea79246eceafb165d79ab21b520 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 22:42:00 +0100 Subject: [PATCH 28/31] test: S11.01 pin the stale-handle branch in FreeIfInUse Adds DestroyOfStaleHandleReportsWarning. Exercises the FreeIfInUse path where IndexFromHandle DOES find a matching slot (so PoolIndexIsValid is true and the lock is taken) but PoolItemIsInUse is false because the slot was already released -- the double-Destroy / use-after-Destroy case introduced by the scan-then-release split. Brings CircularBufferStatic.c to 100% line, 100% function, and 100% branch coverage (68/68, 16/16, 18/18). The whole-codebase line + function coverage stays above the CI floor. Co-Authored-By: Claude Opus 4.7 (1M context) --- Tests/SolidSyslogCircularBufferTest.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tests/SolidSyslogCircularBufferTest.cpp b/Tests/SolidSyslogCircularBufferTest.cpp index f1330319..23fdb590 100644 --- a/Tests/SolidSyslogCircularBufferTest.cpp +++ b/Tests/SolidSyslogCircularBufferTest.cpp @@ -484,3 +484,17 @@ TEST(SolidSyslogCircularBufferPool, DestroyOfUnknownHandleReportsWarning) LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity()); STRCMP_EQUAL(SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY, ErrorHandlerFake_LastMessage()); } + +TEST(SolidSyslogCircularBufferPool, DestroyOfStaleHandleReportsWarning) +{ + pooled[0] = MakeBuffer(); + SolidSyslogCircularBuffer_Destroy(pooled[0]); + ErrorHandlerFake_Install(nullptr); + + SolidSyslogCircularBuffer_Destroy(pooled[0]); + pooled[0] = nullptr; + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity()); + STRCMP_EQUAL(SOLIDSYSLOG_ERROR_MSG_CIRCULARBUFFER_UNKNOWN_DESTROY, ErrorHandlerFake_LastMessage()); +} From 7ae931ae5e07bc8fdd4de65fd65a86854c6ad293 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 23:04:24 +0100 Subject: [PATCH 29/31] test: S11.01 readability pass on SolidSyslogCircularBufferTest Tests now read as a 1-2 line setup followed by an act and a check, with the noise lifted out into shared fixture members and macros. - TEST_BASE CircularBufferFixture holds buffer, readData, readSize and the Write(text) / Write(data, size) / Read() helpers. Groups 1, 2, 3 derive via TEST_GROUP_BASE; each declares only its own ring storage and chooses its mutex source in setup/teardown. Eliminates ~30 lines of duplicated declarations across the three groups. - CHECK_LAST_READ_RECORD(expected, size) macro at file scope replaces the LONGS_EQUAL(size, readSize) + MEMCMP_EQUAL(...) pair that appeared in 8+ tests. Wraps with NOLINTBEGIN/END so the macro preserves __FILE__/__LINE__ at the test's call site. - The Pool group teardown now guards against destroying nullptr pool slots / nullptr overflow -- those previously emitted a WARNING through the still-installed ErrorHandlerFake after the body's assertions, which was harmless but noisy. - Small cleanups: OverflowingWriteIsDropped's local `overflow` -> `extra` (the Pool group has an `overflow` field; the name clash was only visual but it confused the reader). WrapsAroundEndOfStorage drops the inline `enum {CYCLES,...}` in favour of constexpr locals. WriteExceedingMaxMessageSizeIsDropped drops the bespoke destination buffer -- the fixture's readData is fine. 1127 tests pass; static.c coverage stays 100% line / 100% function / 100% branch; cppcheck-misra count stays at 87; clang-tidy, sanitize, clang-format, plain cppcheck all green. Co-Authored-By: Claude Opus 4.7 (1M context) --- Tests/SolidSyslogCircularBufferTest.cpp | 274 +++++++++++++----------- 1 file changed, 145 insertions(+), 129 deletions(-) diff --git a/Tests/SolidSyslogCircularBufferTest.cpp b/Tests/SolidSyslogCircularBufferTest.cpp index 23fdb590..0aea6a3d 100644 --- a/Tests/SolidSyslogCircularBufferTest.cpp +++ b/Tests/SolidSyslogCircularBufferTest.cpp @@ -16,10 +16,22 @@ using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-file scope only; brings ONCE/NEVER into scope for CALLED_FAKE +// Assertion macros at file scope so failures report the test's own +// __FILE__/__LINE__ rather than the helper's. +// NOLINTBEGIN(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) -- macros preserve __FILE__/__LINE__ at the call site + +// Asserts the last record read equals `expected` bytes of length `size`. +// Depends on `readData` and `readSize` from CircularBufferFixture being in scope. +#define CHECK_LAST_READ_RECORD(expected, size) \ + do \ + { \ + LONGS_EQUAL((size), readSize); \ + MEMCMP_EQUAL((expected), readData, (size)); \ + } while (0) + // Asserts buf is a non-null handle that is not one of the slots in pool. // Used to pin the pool-exhaustion Fallback contract: every legitimate // _Create returns either a pool slot or the Fallback singleton, never NULL. -// NOLINTBEGIN(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) -- macros preserve __FILE__/__LINE__ at the call site #define CHECK_IS_FALLBACK(buf, pool) \ do \ { \ @@ -35,29 +47,32 @@ using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-f enum { - TEST_MAX_MESSAGES = 1 + TEST_MAX_MESSAGES = 1, + SMALL_RING_BYTES = 32 }; +// Shared fixture for any TEST_GROUP_BASE that drives one CircularBuffer through +// Write/Read. Each derived group supplies its own ring storage and its own +// setup/teardown (the mutex source differs per group). Test bodies reference +// `buffer`, `readData`, `readSize`, `Write(...)`, `Read()` directly because +// they are inherited members. // clang-format off -TEST_GROUP(SolidSyslogCircularBuffer) +TEST_BASE(CircularBufferFixture) { - uint8_t ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(TEST_MAX_MESSAGES)]; struct SolidSyslogBuffer* buffer = nullptr; - char readData[SOLIDSYSLOG_MAX_MESSAGE_SIZE]; - // cppcheck-suppress variableScope -- member of TEST_GROUP; scope managed by CppUTest macro - size_t readSize; + char readData[SOLIDSYSLOG_MAX_MESSAGE_SIZE] = {}; + size_t readSize = 0; - void setup() override + // Write a null-terminated C string. Size is strlen() -- intended for + // literal-string tests. For binary payloads use the two-arg overload below. + void Write(const char* text) const { - // cppcheck-suppress unreadVariable -- used across TEST_GROUP methods; cppcheck does not model CppUTest macros - buffer = SolidSyslogCircularBuffer_Create(SolidSyslogNullMutex_Create(), ring, sizeof(ring)); - readSize = 0; + SolidSyslogBuffer_Write(buffer, text, strlen(text)); } - void teardown() override + void Write(const void* data, size_t size) const { - SolidSyslogCircularBuffer_Destroy(buffer); - SolidSyslogNullMutex_Destroy(); + SolidSyslogBuffer_Write(buffer, data, size); } bool Read() @@ -68,82 +83,105 @@ TEST_GROUP(SolidSyslogCircularBuffer) // clang-format on +// clang-format off +TEST_GROUP_BASE(SolidSyslogCircularBuffer, CircularBufferFixture) +{ + uint8_t ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(TEST_MAX_MESSAGES)]; + + void setup() override + { + // cppcheck-suppress unreadVariable -- used across TEST_GROUP methods; cppcheck does not model CppUTest macros + buffer = SolidSyslogCircularBuffer_Create(SolidSyslogNullMutex_Create(), ring, sizeof(ring)); + } + + void teardown() override + { + SolidSyslogCircularBuffer_Destroy(buffer); + SolidSyslogNullMutex_Destroy(); + } +}; + +// clang-format on + TEST(SolidSyslogCircularBuffer, CreateDestroyDoesNotCrash) { } TEST(SolidSyslogCircularBuffer, ReadFromEmptyReturnsFalse) { - CHECK_FALSE(SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize)); + CHECK_FALSE(Read()); } TEST(SolidSyslogCircularBuffer, WriteThenReadReturnsTrue) { - SolidSyslogBuffer_Write(buffer, "hello", 5); - CHECK_TRUE(SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize)); + Write("hello"); + + CHECK_TRUE(Read()); } TEST(SolidSyslogCircularBuffer, ReadReturnsWrittenSize) { - SolidSyslogBuffer_Write(buffer, "hello", 5); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); + Write("hello"); + + Read(); + LONGS_EQUAL(5, readSize); } TEST(SolidSyslogCircularBuffer, ReadReturnsWrittenData) { - SolidSyslogBuffer_Write(buffer, "hello", 5); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); + Write("hello"); + + Read(); + MEMCMP_EQUAL("hello", readData, 5); } TEST(SolidSyslogCircularBuffer, SecondReadAfterSingleWriteReturnsFalse) { - SolidSyslogBuffer_Write(buffer, "hello", 5); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); - CHECK_FALSE(SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize)); + Write("hello"); + Read(); + + CHECK_FALSE(Read()); } TEST(SolidSyslogCircularBuffer, TwoWritesReadInOrder) { - SolidSyslogBuffer_Write(buffer, "first", 5); - SolidSyslogBuffer_Write(buffer, "second", 6); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); - MEMCMP_EQUAL("first", readData, 5); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); - MEMCMP_EQUAL("second", readData, 6); + Write("first"); + Write("second"); + + Read(); + CHECK_LAST_READ_RECORD("first", 5); + Read(); + CHECK_LAST_READ_RECORD("second", 6); } TEST(SolidSyslogCircularBuffer, ThreeWritesReadInOrder) { - SolidSyslogBuffer_Write(buffer, "alpha", 5); - SolidSyslogBuffer_Write(buffer, "bravo", 5); - SolidSyslogBuffer_Write(buffer, "charlie", 7); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); - MEMCMP_EQUAL("alpha", readData, 5); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); - MEMCMP_EQUAL("bravo", readData, 5); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); - MEMCMP_EQUAL("charlie", readData, 7); + Write("alpha"); + Write("bravo"); + Write("charlie"); + + Read(); + CHECK_LAST_READ_RECORD("alpha", 5); + Read(); + CHECK_LAST_READ_RECORD("bravo", 5); + Read(); + CHECK_LAST_READ_RECORD("charlie", 7); } TEST(SolidSyslogCircularBuffer, WrapsAroundEndOfStorage) { - enum - { - CYCLES = 400, - PAYLOAD_SIZE = 10 - }; + constexpr size_t payloadSize = 10; + constexpr int cycles = 400; + char payload[payloadSize]; + memset(payload, 'X', payloadSize); - char payload[PAYLOAD_SIZE]; - memset(payload, 'X', PAYLOAD_SIZE); - - for (int i = 0; i < CYCLES; i++) + for (int i = 0; i < cycles; i++) { - SolidSyslogBuffer_Write(buffer, payload, PAYLOAD_SIZE); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); - LONGS_EQUAL(PAYLOAD_SIZE, readSize); - MEMCMP_EQUAL(payload, readData, PAYLOAD_SIZE); + Write(payload, payloadSize); + Read(); + CHECK_LAST_READ_RECORD(payload, payloadSize); } } @@ -151,57 +189,48 @@ TEST(SolidSyslogCircularBuffer, OverflowingWriteIsDropped) { char filler[SOLIDSYSLOG_MAX_MESSAGE_SIZE]; memset(filler, 'A', sizeof(filler)); - char overflow[100]; - memset(overflow, 'B', sizeof(overflow)); + char extra[100]; + memset(extra, 'B', sizeof(extra)); - SolidSyslogBuffer_Write(buffer, filler, sizeof(filler)); - SolidSyslogBuffer_Write(buffer, overflow, sizeof(overflow)); + Write(filler, sizeof(filler)); + Write(extra, sizeof(extra)); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); - LONGS_EQUAL(sizeof(filler), readSize); - MEMCMP_EQUAL(filler, readData, sizeof(filler)); - CHECK_FALSE(SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize)); + Read(); + CHECK_LAST_READ_RECORD(filler, sizeof(filler)); + CHECK_FALSE(Read()); } TEST(SolidSyslogCircularBuffer, WriteAfterDrainAcceptsRecordTooLargeForRemainingTailSpace) { - SolidSyslogBuffer_Write(buffer, "x", 1); - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); + Write("x"); + Read(); char big[SOLIDSYSLOG_MAX_MESSAGE_SIZE]; memset(big, 'B', sizeof(big)); - SolidSyslogBuffer_Write(buffer, big, sizeof(big)); + Write(big, sizeof(big)); - CHECK_TRUE(SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize)); - LONGS_EQUAL(sizeof(big), readSize); - MEMCMP_EQUAL(big, readData, sizeof(big)); + CHECK_TRUE(Read()); + CHECK_LAST_READ_RECORD(big, sizeof(big)); } TEST(SolidSyslogCircularBuffer, WriteExceedingMaxMessageSizeIsDropped) { char tooBig[SOLIDSYSLOG_MAX_MESSAGE_SIZE + 1]; memset(tooBig, 'X', sizeof(tooBig)); - SolidSyslogBuffer_Write(buffer, tooBig, sizeof(tooBig)); + Write(tooBig, sizeof(tooBig)); - char bigDest[SOLIDSYSLOG_MAX_MESSAGE_SIZE + 1]; - size_t got = 0; - CHECK_FALSE(SolidSyslogBuffer_Read(buffer, bigDest, sizeof(bigDest), &got)); + CHECK_FALSE(Read()); } // clang-format off -TEST_GROUP(SolidSyslogCircularBufferMutex) +TEST_GROUP_BASE(SolidSyslogCircularBufferMutex, CircularBufferFixture) { - uint8_t ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(TEST_MAX_MESSAGES)]; - struct SolidSyslogBuffer* buffer = nullptr; - char readData[SOLIDSYSLOG_MAX_MESSAGE_SIZE]; - // cppcheck-suppress variableScope -- member of TEST_GROUP; scope managed by CppUTest macro - size_t readSize; + uint8_t ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(TEST_MAX_MESSAGES)]; void setup() override { // cppcheck-suppress unreadVariable -- used across TEST_GROUP methods; cppcheck does not model CppUTest macros - buffer = SolidSyslogCircularBuffer_Create(MutexFake_Create(), ring, sizeof(ring)); - readSize = 0; + buffer = SolidSyslogCircularBuffer_Create(MutexFake_Create(), ring, sizeof(ring)); } void teardown() override @@ -209,46 +238,33 @@ TEST_GROUP(SolidSyslogCircularBufferMutex) SolidSyslogCircularBuffer_Destroy(buffer); MutexFake_Destroy(); } - - bool Read() - { - return SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); - } }; // clang-format on TEST(SolidSyslogCircularBufferMutex, WriteCallsLockThenUnlockOnce) { - SolidSyslogBuffer_Write(buffer, "hello", 5); + Write("hello"); + STRCMP_EQUAL("LU", MutexFake_Sequence()); } TEST(SolidSyslogCircularBufferMutex, ReadCallsLockThenUnlockOnce) { - SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); + Read(); + STRCMP_EQUAL("LU", MutexFake_Sequence()); } -enum -{ - SMALL_RING_BYTES = 32 -}; - // clang-format off -TEST_GROUP(SolidSyslogCircularBufferSmallRing) +TEST_GROUP_BASE(SolidSyslogCircularBufferSmallRing, CircularBufferFixture) { - uint8_t ring[SMALL_RING_BYTES]; - struct SolidSyslogBuffer* buffer = nullptr; - char readData[SMALL_RING_BYTES]; - // cppcheck-suppress variableScope -- member of TEST_GROUP; scope managed by CppUTest macro - size_t readSize; + uint8_t ring[SMALL_RING_BYTES]; void setup() override { // cppcheck-suppress unreadVariable -- used across TEST_GROUP methods; cppcheck does not model CppUTest macros - buffer = SolidSyslogCircularBuffer_Create(SolidSyslogNullMutex_Create(), ring, sizeof(ring)); - readSize = 0; + buffer = SolidSyslogCircularBuffer_Create(SolidSyslogNullMutex_Create(), ring, sizeof(ring)); } void teardown() override @@ -256,11 +272,6 @@ TEST_GROUP(SolidSyslogCircularBufferSmallRing) SolidSyslogCircularBuffer_Destroy(buffer); SolidSyslogNullMutex_Destroy(); } - - bool Read() - { - return SolidSyslogBuffer_Read(buffer, readData, sizeof(readData), &readSize); - } }; // clang-format on @@ -274,10 +285,10 @@ TEST(SolidSyslogCircularBufferSmallRing, WrapWriteFillingExactlyToHeadDoesNotCol { char rec[12]; memset(rec, 'A', sizeof(rec)); - SolidSyslogBuffer_Write(buffer, rec, sizeof(rec)); - SolidSyslogBuffer_Write(buffer, rec, sizeof(rec)); + Write(rec, sizeof(rec)); + Write(rec, sizeof(rec)); Read(); - SolidSyslogBuffer_Write(buffer, rec, sizeof(rec)); + Write(rec, sizeof(rec)); CHECK_TRUE(Read()); LONGS_EQUAL(sizeof(rec), readSize); @@ -299,22 +310,21 @@ TEST(SolidSyslogCircularBufferSmallRing, WriteInWrappedStateFillingExactlyToHead memset(d, 'd', sizeof(d)); memset(e, 'e', sizeof(e)); - SolidSyslogBuffer_Write(buffer, a, sizeof(a)); - SolidSyslogBuffer_Write(buffer, b, sizeof(b)); + Write(a, sizeof(a)); + Write(b, sizeof(b)); Read(); - SolidSyslogBuffer_Write(buffer, c, sizeof(c)); + Write(c, sizeof(c)); Read(); - SolidSyslogBuffer_Write(buffer, d, sizeof(d)); - SolidSyslogBuffer_Write(buffer, e, sizeof(e)); + Write(d, sizeof(d)); + Write(e, sizeof(e)); CHECK_TRUE(Read()); - LONGS_EQUAL(sizeof(c), readSize); - MEMCMP_EQUAL(c, readData, sizeof(c)); + CHECK_LAST_READ_RECORD(c, sizeof(c)); } TEST(SolidSyslogCircularBufferSmallRing, ReadIntoSmallerBufferReturnsFalseAndLeavesRecordQueued) { - SolidSyslogBuffer_Write(buffer, "hello", 5); + Write("hello"); char dest[5]; size_t got = 0; @@ -322,8 +332,7 @@ TEST(SolidSyslogCircularBufferSmallRing, ReadIntoSmallerBufferReturnsFalseAndLea LONGS_EQUAL(0, got); CHECK_TRUE(Read()); - LONGS_EQUAL(5, readSize); - MEMCMP_EQUAL("hello", readData, 5); + CHECK_LAST_READ_RECORD("hello", 5); } // Exercises the ConsumeWrapMarker branch of Read: write A, B, drain A, write @@ -340,18 +349,18 @@ TEST(SolidSyslogCircularBufferSmallRing, WrappedBufferReadsAllRecordsInOrder) memset(c, 'c', sizeof(c)); memset(d, 'd', sizeof(d)); - SolidSyslogBuffer_Write(buffer, a, sizeof(a)); - SolidSyslogBuffer_Write(buffer, b, sizeof(b)); + Write(a, sizeof(a)); + Write(b, sizeof(b)); Read(); - SolidSyslogBuffer_Write(buffer, c, sizeof(c)); - SolidSyslogBuffer_Write(buffer, d, sizeof(d)); + Write(c, sizeof(c)); + Write(d, sizeof(d)); CHECK_TRUE(Read()); - MEMCMP_EQUAL(b, readData, sizeof(b)); + CHECK_LAST_READ_RECORD(b, sizeof(b)); CHECK_TRUE(Read()); - MEMCMP_EQUAL(c, readData, sizeof(c)); + CHECK_LAST_READ_RECORD(c, sizeof(c)); CHECK_TRUE(Read()); - MEMCMP_EQUAL(d, readData, sizeof(d)); + CHECK_LAST_READ_RECORD(d, sizeof(d)); CHECK_FALSE(Read()); } @@ -359,9 +368,9 @@ TEST(SolidSyslogCircularBufferSmallRing, WrappedBufferReadsAllRecordsInOrder) TEST_GROUP(SolidSyslogCircularBufferPool) { uint8_t ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(1)]; - struct SolidSyslogMutex* mutex = nullptr; - struct SolidSyslogBuffer* pooled[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE] = {}; - struct SolidSyslogBuffer* overflow = nullptr; + struct SolidSyslogMutex* mutex = nullptr; + struct SolidSyslogBuffer* pooled[SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE] = {}; + struct SolidSyslogBuffer* overflow = nullptr; void setup() override { @@ -371,11 +380,17 @@ TEST_GROUP(SolidSyslogCircularBufferPool) void teardown() override { - for (auto* buffer : pooled) + for (auto* handle : pooled) + { + if (handle != nullptr) + { + SolidSyslogCircularBuffer_Destroy(handle); + } + } + if (overflow != nullptr) { - SolidSyslogCircularBuffer_Destroy(buffer); + SolidSyslogCircularBuffer_Destroy(overflow); } - SolidSyslogCircularBuffer_Destroy(overflow); SolidSyslogNullMutex_Destroy(); ConfigLockFake_Uninstall(); ErrorHandlerFake_Uninstall(); @@ -400,6 +415,7 @@ TEST_GROUP(SolidSyslogCircularBufferPool) TEST(SolidSyslogCircularBufferPool, FillingPoolThenOverflowReturnsDistinctFallback) { FillPool(); + overflow = MakeBuffer(); CHECK_IS_FALLBACK(overflow, pooled); From 286194b374244136a8f10d882dceb91f6b4f64cd Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 17 May 2026 23:05:28 +0100 Subject: [PATCH 30/31] docs: update DEVLOG for S11.01 pool migration body Captures the locked-in shape, the reference structure of *Static.c (this is the E11 reference for every other class), the test pinning strategy, the mid-flight course correction on helper extraction, the gate posture, and the open questions. Co-Authored-By: Claude Opus 4.7 (1M context) --- DEVLOG.md | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/DEVLOG.md b/DEVLOG.md index b512448b..339e649d 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,155 @@ # Dev Log +## 2026-05-17 — S11.01 commits 2–N: CircularBuffer pool migration (E11 pilot, #392) + +The body of S11.01 — every commit after the `SolidSyslog_SetConfigLock` +seam landed in #393. Split across 28 commits on +`feat/s11-01-circular-buffer-pool` so each step can be reviewed +independently; the PR ships them squashed. + +### Locked-in shape (this is the reference for every other E11 class) + +- **Three-TU split per class.** `SolidSyslogCircularBuffer.c` holds the + vtable + ring logic + private `_Initialise` / `_Cleanup`. + `SolidSyslogCircularBufferPrivate.h` (TU-internal) declares the + struct + private signatures. `SolidSyslogCircularBufferStatic.c` + owns the pool, the public `_Create` / `_Destroy`, the + fallback-handle plumbing, and all slot-walk synchronisation. +- **Tunable** `SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE` (default 1U) + in `SolidSyslogTunablesDefaults.h` with `#ifndef` override and a + floor `#error`. +- **LockConfig pair injection** wraps every slot probe (in both + Acquire and Free walks). `Initialise` runs *outside* the lock so a + future expensive `_Initialise` (FatFs open, mbedTLS setup) doesn't + block interrupts when the integrator maps `SetConfigLock` to + `taskENTER_CRITICAL`. `Cleanup` stays *inside* its per-iteration + lock — releasing the lock around it would let a concurrent Create + grab the slot and race Initialise vs Cleanup. +- **Fallback singleton** of type `struct SolidSyslogBuffer` with a + no-op vtable. Pool exhaustion returns `&Fallback` (the integrator's + Log/Service calls become no-ops, never NULL deref). `_Create` + reports `SOLIDSYSLOG_SEVERITY_ERROR` once on the transition; the + integrator hooks `SolidSyslog_SetErrorHandler` to see it. +- **`_Destroy` is uniform**: any handle not currently issued by + Create — unknown stranger, pool-issued but already destroyed, + Fallback — reports `SOLIDSYSLOG_SEVERITY_WARNING`. The earlier + attempt to special-case Fallback as silent collapsed; the simpler + rule survives. +- **Symmetric `Initialise` / `Cleanup` API.** Both take + `struct SolidSyslogBuffer* base` and recover the concrete type via + `SelfFromBase` internally. The 11.3 cast lives in exactly one TU + (already deviated). `_Static.c` never casts. +- **Scan-then-release in `_Destroy`.** The address-match walk is + lock-free (pool addresses are file-scope statics, never change), + and the lock only wraps the InUse-check + Cleanup + MarkFree. + Lock-count drops to **0** on unknown handles, **1** on matched + handles, regardless of pool size. + +### Reference shape of `*Static.c` + +`_Create` and `_Destroy` each read as four lines of pure intent +against named helpers. The helper tree splits cleanly: + +- **Composition**: `AcquireFirstFree`, `AcquireIfFree`, + `IndexFromHandle`, `FreeIfInUse`, `Acquire` +- **Predicates** (subject-IS-adjective form): + `PoolItemIsFree` (chains through `PoolItemIsInUse`), + `PoolItemIsInUse`, `HandleIsValid`, `PoolIndexIsValid` +- **Atomic field ops**: `MarkInUse`, `MarkFree`, + `HandleFromIndex`, `PoolItemIsInUse` + +The `.InUse` flag is read in exactly one place (`PoolItemIsInUse`), +set-true in one place (`MarkInUse`), set-false in one place +(`MarkFree`). `&Fallback` is referenced in two places (the static +initialiser and `HandleIsValid`). `&Pool[poolIndex].Object.Base` is +computed in exactly one place (`HandleFromIndex`). + +### Tests + +The Pool TEST_GROUP pins: +- pool-exhaustion returns a distinct Fallback handle + (`FillingPoolThenOverflowReturnsDistinctFallback`) +- pool-exhaustion reports ERROR exactly once +- the Fallback vtable methods are no-ops +- `_Create` locks the right number of times under empty / full pool +- `_Destroy` locks exactly once on a pool-issued handle, zero times + on an unknown handle +- unknown and stale handles report WARNING with the right message + +100% line / 100% function / 100% branch coverage on +`SolidSyslogCircularBufferStatic.c` (68/68, 16/16, 18/18). + +The basic CircularBuffer TEST_GROUPs were also restructured: a shared +`CircularBufferFixture` TEST_BASE holds `buffer`, `readData`, +`readSize`, and the `Write(...)` / `Read()` helpers; a +`CHECK_LAST_READ_RECORD` macro replaces the +`LONGS_EQUAL + MEMCMP_EQUAL` pair at 8+ sites. Test bodies now read +top-to-bottom as setup → act → check with no `SolidSyslogBuffer_Write +(buffer, ..., sizeof(...), &readSize)` boilerplate. + +### Notable course-correction mid-flight + +Mid-pass I had extracted three static-inline helpers +(`TryClaim` / `Release` / `SlotOwnsBase`) into `*Static.c`. David +flagged that the file got harder to read, not easier, and asked for +the extractions to be reverted to a flat-inline shape. We then +re-extracted from a clean baseline, one named helper at a time, +each commit prompted and reviewed. The resulting file is shorter +and reads top-down at one level of abstraction per function. The +pattern memory `project_e11_three_tu_split.md` already records the +three-TU split; the helper-extraction lesson is preserved in +`feedback_no_premature_generalisation.md`. + +### Gate posture + +- cppcheck-misra: **87** (baseline 88 on main, improved by 1 via + `_Destroy` becoming single-exit and then by the scan-then-release + split removing the locked walk's compound condition). +- No new `cppcheck-suppress` / `NOLINT` additions on the branch. + The line-pinned 11.3 suppression for `CircularBuffer.c`'s + `SelfFromBase` moved twice (cast shifted 1 line each time + Initialise / Cleanup signature changed); both shifts updated the + existing entry in `misra_suppressions.txt`, never added new ones. +- All other gates green: debug, clang-debug, sanitize, tidy, + cppcheck, coverage, clang-format, integration-linux-openssl + (host tests only — CI covers Windows / BDD / FreeRTOS). + +### Decisions locked in for E11 + +- `CircularBuffer_AcquireIfFree` shape (single `poolIndex` argument, + returns handle-or-Fallback) is the reference for per-slot probes + in every E11 class. +- `IndexFromHandle` + `FreeIfInUse` shape in `_Destroy` is the + reference for any class whose handles can be released by the + integrator. +- "Initialise outside lock, Cleanup inside lock" asymmetry is + intentional and applies to every E11 class. +- Pool size 1 by default; integrators bump via + `SOLIDSYSLOG_USER_TUNABLES_FILE`. + +### Deferred + +- Branch-coverage gate is *not* turned on at CI level — the local + run had to re-capture with `--rc lcov_branch_coverage=1` to + surface the 18/18 figure. Switching the coverage preset to capture + branches by default is a separate housekeeping story. +- A `tunable-override-debug` preset that bumps pool size > 1 — would + pin the per-iteration locking observably in Create's + `CreateLocksOncePerSlotProbedWhenPoolIsFull` test. Useful but not + blocking; tests assert in terms of `SOLIDSYSLOG_CIRCULAR_BUFFER_POOL_SIZE` + so they still hold at any pool size. + +### Open questions + +- Whether `PoolItemIsInUse` should be exposed in `*Private.h` for + any future test that wants to probe state from outside `*Static.c`. + Not needed yet; the file-scope visibility is correct as is. +- Whether the symmetric `HandleIsInvalid` / `PoolItemIsInUse` / + `MarkFree` helpers should be lifted into a shared header for + reuse across E11 classes, or whether each class re-implements them + privately. Defer until the second E11 class lands and we see what + actually duplicates. + ## 2026-05-17 — S11.01 commit 1: `SolidSyslog_SetConfigLock` injection point (#392) First commit of S11.01 (the E11 pilot). Lands the global lock/unlock From 9bd1d3928a42087c2c7ea5025346d3a7a884fcd5 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 18 May 2026 06:41:29 +0100 Subject: [PATCH 31/31] fix: S11.01 IWYU + CodeRabbit accepted feedback Addresses the analyze-iwyu CI failure and the two actionable CodeRabbit comments worth acting on: - Tests/SolidSyslogCircularBufferTest.cpp: explicit `#include ` for uint8_t. The new TEST_GROUP_BASE-derived groups declare `uint8_t ring[...]` directly; uint8_t previously came in transitively through a SolidSyslog header. (IWYU finding.) - scripts/iwyu_filter.py: extend the filter to drop CppUTest macro-generated `class TEST___Test;` forward-decl additions. Same category of false positive as the existing `- #include "CppUTest/TestHarness.h"` filter -- IWYU doesn't model CppUTest's TEST() / TEST_GROUP_BASE() macro expansion. Filter smoke-tested locally; running IWYU in the clang container reports every TU "has correct #includes/fwd-decls" with no actionable findings. - docs/misra-deviations.md (RING_BYTES section): reword "caller-supplied ring memory in messages" -> "caller-supplied ring buffer in bytes, derived from a maximum message count" with a worked example. The macro returns bytes; the old wording was unit-ambiguous. (CodeRabbit minor.) - docs/misra-deviations.md (D.002 section): extend the E11-exception paragraph to explicitly note that the surviving 11.3 suppression for Core/Source/SolidSyslogCircularBuffer.c covers the vtable downcast inside CircularBuffer_SelfFromBase, not the caller-storage void* cast that's now gone. Same deviation block continues to cover both shapes because both are 11.3 firings driven by the same "interface decoupled from concrete type" design. (CodeRabbit major -- accepted as a documentation/comment update rather than removal; the cast itself is the standard OO-in-C vtable downcast that every other class in the deviation already shares.) The three other CodeRabbit findings (Destroy-warns-on-Fallback, function-order, "unused" SolidSyslogPrival.h) are addressed by reply on the PR rather than code change -- see PR #394 thread. 1127 tests pass; IWYU clean; cppcheck-misra count stays at 87. Co-Authored-By: Claude Opus 4.7 (1M context) --- Tests/SolidSyslogCircularBufferTest.cpp | 1 + docs/misra-deviations.md | 16 ++++++++++- scripts/iwyu_filter.py | 36 ++++++++++++++++++++----- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/Tests/SolidSyslogCircularBufferTest.cpp b/Tests/SolidSyslogCircularBufferTest.cpp index 0aea6a3d..7730aaf9 100644 --- a/Tests/SolidSyslogCircularBufferTest.cpp +++ b/Tests/SolidSyslogCircularBufferTest.cpp @@ -1,4 +1,5 @@ #include +#include #include "CppUTest/TestHarness.h" diff --git a/docs/misra-deviations.md b/docs/misra-deviations.md index 9d621980..9fe51d10 100644 --- a/docs/misra-deviations.md +++ b/docs/misra-deviations.md @@ -172,6 +172,17 @@ plain `uint8_t* ring, size_t ringBytes`. No `void*` storage cast on the instance; the ring pointer is held untyped inside the impl struct. +The 11.3 suppression listed against `Core/Source/SolidSyslogCircularBuffer.c` +post-E11 is therefore narrower than the rest of the entries above: it +covers only the **vtable downcast** inside `CircularBuffer_SelfFromBase` +(`struct SolidSyslogBuffer*` → `struct SolidSyslogCircularBuffer*`), the +standard OO-in-C "interface pointer back to derived implementation" cast +that every vtable method needs. The same downcast survives in every +caller-storage class listed above as a separate concern from the +caller-storage void* cast; this deviation covers both shapes under one +heading because both are MISRA 11.3 firings driven by the same +"interface decoupled from concrete type" design. + ### Risk and mitigation - **Type safety** — Each `_Create` is the only place in the library @@ -832,7 +843,10 @@ Bdd/Targets/FreeRtos/main.c — RING_BYTES ``` The macro is part of the public API; integrators use it to size -caller-supplied ring memory in messages. +the caller-supplied ring buffer in bytes, derived from a maximum +message count (e.g. `uint8_t ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(4)]` +allocates enough bytes for four full-size messages plus their +length headers). The alternatives all regress: diff --git a/scripts/iwyu_filter.py b/scripts/iwyu_filter.py index 6786a3e4..60e26c49 100755 --- a/scripts/iwyu_filter.py +++ b/scripts/iwyu_filter.py @@ -2,9 +2,11 @@ """Filter IWYU output to drop categories that don't apply to this project. IWYU's analysis is enabled in CI as a ratchet against include-hygiene drift. -Three classes of 'should remove' finding are filtered out before the gate -evaluates, because each represents IWYU producing the wrong answer for this -codebase rather than a real drift: +Several classes of finding are filtered out before the gate evaluates, +because each represents IWYU producing the wrong answer for this codebase +rather than a real drift: + +REMOVALS that we keep as-is: 1. '- struct ;' — explicit forward declarations. In C, 'struct Foo* member;' inside another struct definition itself @@ -25,10 +27,18 @@ It thinks the include is unused because the symbols it provides are only invoked via macros. Removing it would break every test file. +ADDITIONS that IWYU asks for but we never write by hand: + +4. 'class TEST___Test;' — CppUTest macro-generated classes. + TEST() / TEST_GROUP() / TEST_GROUP_BASE() expand to class declarations + that IWYU sees but can't trace back to the macros that produce them, + so it asks for forward declarations no test file ever writes. Same + shape of false positive as (3) — IWYU not modelling CppUTest macros. + Reads IWYU output (typically the stdout of iwyu_tool.py) on stdin, emits filtered output on stdout. File blocks whose only findings are filtered -removals are suppressed entirely, so the report shows only actionable -findings. +findings are suppressed entirely, so the report shows only actionable +ones. Exit code: 0 — no actionable findings remain after filtering @@ -44,11 +54,19 @@ re.compile(r"^- #include \"CppUTest/TestHarness\.h\"\s*//"), ) +FILTERED_ADDITIONS = ( + re.compile(r"^class TEST_\w+_Test;\s*$"), +) + def _is_filtered_removal(line): return any(pat.match(line) for pat in FILTERED_REMOVALS) +def _is_filtered_addition(line): + return any(pat.match(line) for pat in FILTERED_ADDITIONS) + + def filter_iwyu(stream_in, stream_out): """Filter IWYU output; return count of files with actionable findings.""" lines = stream_in.readlines() @@ -107,7 +125,10 @@ def _filter_block(block): elif section == "tail": tail_lines.append(line) - add_real = [l for l in add_lines if l.strip()] + add_real = [ + l for l in add_lines + if l.strip() and not _is_filtered_addition(l) + ] remove_real = [ l for l in remove_lines if l.strip() and not _is_filtered_removal(l) @@ -117,7 +138,8 @@ def _filter_block(block): return [], False out = [header] - out.extend(add_lines) + if add_real: + out.extend(add_real) out.append("\n") out.append(header.replace(" should add these lines:", " should remove these lines:")) if remove_real: