From 31a3b40cfeef3344bd7c58a91bac9f8b00535461 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 3 Jun 2026 21:27:30 +0100 Subject: [PATCH 1/8] feat: S29.04 PlusFat adapter pack scaffolding + proven pool baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stand up Platform/PlusFat/ (INTERFACE lib, gated on FREERTOS_PLUS_FAT_PATH) and the Tests/PlusFat host harness. The pool plumbing (Static.c) and its 9-test suite are copied + renamed verbatim from the FatFs sibling — proven, agreed coverage. The class seam (Initialise/Cleanup) is a minimal stub; the ff_* behaviour is driven by TDD from here. PlusFatFakes carries the test-local FreeRTOSFATConfig.h; the shared FreeRtosFakes FreeRTOSConfig.h gains configNUM_THREAD_LOCAL_STORAGE_POINTERS (Plus-FAT's ff_stdio requires >= 3). Plus-FAT is FreeRTOS-coupled and the test include path surfaces that rather than hiding it. Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 7 + Platform/PlusFat/CMakeLists.txt | 25 +++ .../Interface/SolidSyslogPlusFatFile.h | 15 ++ .../Interface/SolidSyslogPlusFatFileErrors.h | 21 +++ .../PlusFat/Source/SolidSyslogPlusFatFile.c | 21 +++ .../Source/SolidSyslogPlusFatFilePrivate.h | 28 +++ .../Source/SolidSyslogPlusFatFileStatic.c | 78 ++++++++ Tests/CMakeLists.txt | 13 ++ Tests/PlusFat/CMakeLists.txt | 50 +++++ .../SolidSyslogPlusFatFilePoolTest.cpp | 173 ++++++++++++++++++ Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp | 38 ++++ Tests/PlusFat/main.cpp | 6 + .../FreeRtosFakes/Interface/FreeRTOSConfig.h | 8 + Tests/Support/PlusFatFakes/CMakeLists.txt | 15 ++ .../Interface/FreeRTOSFATConfig.h | 22 +++ .../PlusFatFakes/Interface/PlusFatFake.h | 12 ++ .../Support/PlusFatFakes/Source/PlusFatFake.c | 7 + 17 files changed, 539 insertions(+) create mode 100644 Platform/PlusFat/CMakeLists.txt create mode 100644 Platform/PlusFat/Interface/SolidSyslogPlusFatFile.h create mode 100644 Platform/PlusFat/Interface/SolidSyslogPlusFatFileErrors.h create mode 100644 Platform/PlusFat/Source/SolidSyslogPlusFatFile.c create mode 100644 Platform/PlusFat/Source/SolidSyslogPlusFatFilePrivate.h create mode 100644 Platform/PlusFat/Source/SolidSyslogPlusFatFileStatic.c create mode 100644 Tests/PlusFat/CMakeLists.txt create mode 100644 Tests/PlusFat/SolidSyslogPlusFatFilePoolTest.cpp create mode 100644 Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp create mode 100644 Tests/PlusFat/main.cpp create mode 100644 Tests/Support/PlusFatFakes/CMakeLists.txt create mode 100644 Tests/Support/PlusFatFakes/Interface/FreeRTOSFATConfig.h create mode 100644 Tests/Support/PlusFatFakes/Interface/PlusFatFake.h create mode 100644 Tests/Support/PlusFatFakes/Source/PlusFatFake.c diff --git a/CMakeLists.txt b/CMakeLists.txt index ada05bcc..02d9992e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -304,6 +304,13 @@ if(DEFINED ENV{FATFS_PATH}) add_subdirectory(Platform/FatFs) endif() +# FreeRTOS-Plus-FAT adapters ship as sources via an INTERFACE library too — the +# integrator's FreeRTOSFATConfig.h flows into each consumer's compile. Gated on +# the upstream Plus-FAT sources being present in the build environment. +if(DEFINED ENV{FREERTOS_PLUS_FAT_PATH}) + add_subdirectory(Platform/PlusFat) +endif() + # The cross-compiled FreeRTOS target is selected by the arm-none-eabi # toolchain file (see Bdd/Targets/FreeRtos/cmake/arm-none-eabi.cmake); it # only enters the build when a freertos-cross preset is active. The chosen diff --git a/Platform/PlusFat/CMakeLists.txt b/Platform/PlusFat/CMakeLists.txt new file mode 100644 index 00000000..cf33bae6 --- /dev/null +++ b/Platform/PlusFat/CMakeLists.txt @@ -0,0 +1,25 @@ +# FreeRTOS-Plus-FAT adapter pack — shipped as sources, not a precompiled library. +# +# Plus-FAT is header-configured: the integrator's FreeRTOSFATConfig.h flows +# through upstream headers and changes the size/layout of the FF_FILE / +# FF_IOManager objects and the visible API surface. Plus-FAT is also FreeRTOS- +# coupled (ff_headers.h pulls FreeRTOS.h / task.h / semphr.h), so compiling our +# adapter once into libSolidSyslog.a would silently break consumers that use a +# different FreeRTOSFATConfig.h or kernel config. +# +# Instead, this is an INTERFACE library — each consumer (Bdd/Targets/FreeRtos, +# Tests/PlusFat/*Test, downstream integrator apps) recompiles the adapter +# sources with its own FreeRTOSFATConfig.h on the include path. Mirrors the +# Platform/FatFs/ and Platform/FreeRtos/ packs — see +# project_header_configured_platforms memory. + +add_library(SolidSyslogPlusFat INTERFACE) + +target_include_directories(SolidSyslogPlusFat INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/Interface +) + +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/Interface/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILES_MATCHING PATTERN "*.h" +) diff --git a/Platform/PlusFat/Interface/SolidSyslogPlusFatFile.h b/Platform/PlusFat/Interface/SolidSyslogPlusFatFile.h new file mode 100644 index 00000000..3251e8bd --- /dev/null +++ b/Platform/PlusFat/Interface/SolidSyslogPlusFatFile.h @@ -0,0 +1,15 @@ +#ifndef SOLIDSYSLOGPLUSFATFILE_H +#define SOLIDSYSLOGPLUSFATFILE_H + +#include "ExternC.h" + +struct SolidSyslogFile; + +EXTERN_C_BEGIN + + struct SolidSyslogFile* SolidSyslogPlusFatFile_Create(void); + void SolidSyslogPlusFatFile_Destroy(struct SolidSyslogFile * base); + +EXTERN_C_END + +#endif /* SOLIDSYSLOGPLUSFATFILE_H */ diff --git a/Platform/PlusFat/Interface/SolidSyslogPlusFatFileErrors.h b/Platform/PlusFat/Interface/SolidSyslogPlusFatFileErrors.h new file mode 100644 index 00000000..47eb9806 --- /dev/null +++ b/Platform/PlusFat/Interface/SolidSyslogPlusFatFileErrors.h @@ -0,0 +1,21 @@ +#ifndef SOLIDSYSLOGPLUSFATFILEERRORS_H +#define SOLIDSYSLOGPLUSFATFILEERRORS_H + +#include "ExternC.h" + +EXTERN_C_BEGIN + + struct SolidSyslogErrorSource; + + enum SolidSyslogPlusFatFileErrors + { + PLUSFATFILE_ERROR_POOL_EXHAUSTED, + PLUSFATFILE_ERROR_UNKNOWN_DESTROY, + PLUSFATFILE_ERROR_MAX + }; + + extern const struct SolidSyslogErrorSource PlusFatFileErrorSource; + +EXTERN_C_END + +#endif /* SOLIDSYSLOGPLUSFATFILEERRORS_H */ diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c new file mode 100644 index 00000000..93bd86ed --- /dev/null +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c @@ -0,0 +1,21 @@ +#include "SolidSyslogPlusFatFile.h" + +#include "SolidSyslogFileDefinition.h" +#include "SolidSyslogNullFile.h" +#include "SolidSyslogPlusFatFileErrors.h" +#include "SolidSyslogPlusFatFilePrivate.h" + +const struct SolidSyslogErrorSource PlusFatFileErrorSource = {"PlusFatFile"}; + +void PlusFatFile_Initialise(struct SolidSyslogFile* base) +{ + /* Vtable wiring is grown by the behavioural TDD slices. */ + (void) base; +} + +void PlusFatFile_Cleanup(struct SolidSyslogFile* base) +{ + /* Overwrite the abstract base with the shared NullFile vtable so + * use-after-destroy is a safe no-op rather than a NULL-fn-pointer crash. */ + *base = *SolidSyslogNullFile_Get(); +} diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFilePrivate.h b/Platform/PlusFat/Source/SolidSyslogPlusFatFilePrivate.h new file mode 100644 index 00000000..d4686f8d --- /dev/null +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFilePrivate.h @@ -0,0 +1,28 @@ +#ifndef SOLIDSYSLOGPLUSFATFILEPRIVATE_H +#define SOLIDSYSLOGPLUSFATFILEPRIVATE_H + +#include + +#include "SolidSyslogError.h" +#include "SolidSyslogFileDefinition.h" +#include "SolidSyslogPlusFatFileErrors.h" +#include "SolidSyslogPrival.h" + +struct SolidSyslogPlusFatFile +{ + struct SolidSyslogFile Base; +}; + +void PlusFatFile_Initialise(struct SolidSyslogFile* base); +void PlusFatFile_Cleanup(struct SolidSyslogFile* base); + +static inline void PlusFatFile_Report( + enum SolidSyslogSeverity severity, + uint16_t category, + enum SolidSyslogPlusFatFileErrors code +) +{ + SolidSyslog_Error(severity, &PlusFatFileErrorSource, category, (int32_t) code); +} + +#endif /* SOLIDSYSLOGPLUSFATFILEPRIVATE_H */ diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFileStatic.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFileStatic.c new file mode 100644 index 00000000..4097675c --- /dev/null +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFileStatic.c @@ -0,0 +1,78 @@ +#include "SolidSyslogPlusFatFile.h" + +#include +#include +#include + +#include "SolidSyslogError.h" +#include "SolidSyslogErrorCategory.h" +#include "SolidSyslogNullFile.h" +#include "SolidSyslogPlusFatFileErrors.h" +#include "SolidSyslogPlusFatFilePrivate.h" +#include "SolidSyslogPoolAllocator.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogTunables.h" + +struct SolidSyslogFile; + +static inline size_t PlusFatFile_IndexFromHandle(const struct SolidSyslogFile* base); +static inline void PlusFatFile_CleanupAtIndex(size_t index, void* context); + +static bool PlusFatFile_InUse[SOLIDSYSLOG_FILE_POOL_SIZE]; +static struct SolidSyslogPlusFatFile PlusFatFile_Pool[SOLIDSYSLOG_FILE_POOL_SIZE]; +static struct SolidSyslogPoolAllocator PlusFatFile_Allocator = {PlusFatFile_InUse, SOLIDSYSLOG_FILE_POOL_SIZE}; + +struct SolidSyslogFile* SolidSyslogPlusFatFile_Create(void) +{ + size_t index = SolidSyslogPoolAllocator_AcquireFirstFree(&PlusFatFile_Allocator); + struct SolidSyslogFile* handle = SolidSyslogNullFile_Get(); + if (SolidSyslogPoolAllocator_IndexIsValid(&PlusFatFile_Allocator, index) == true) + { + PlusFatFile_Initialise(&PlusFatFile_Pool[index].Base); + handle = &PlusFatFile_Pool[index].Base; + } + else + { + PlusFatFile_Report( + SOLIDSYSLOG_SEVERITY_ERROR, + SOLIDSYSLOG_CAT_POOL_EXHAUSTED, + PLUSFATFILE_ERROR_POOL_EXHAUSTED + ); + } + return handle; +} + +void SolidSyslogPlusFatFile_Destroy(struct SolidSyslogFile* base) +{ + size_t index = PlusFatFile_IndexFromHandle(base); + bool released = SolidSyslogPoolAllocator_IndexIsValid(&PlusFatFile_Allocator, index) && + SolidSyslogPoolAllocator_FreeIfInUse(&PlusFatFile_Allocator, index, PlusFatFile_CleanupAtIndex, NULL); + if (!released) + { + PlusFatFile_Report( + SOLIDSYSLOG_SEVERITY_WARNING, + SOLIDSYSLOG_CAT_UNKNOWN_DESTROY, + PLUSFATFILE_ERROR_UNKNOWN_DESTROY + ); + } +} + +static inline size_t PlusFatFile_IndexFromHandle(const struct SolidSyslogFile* base) +{ + size_t result = SOLIDSYSLOG_FILE_POOL_SIZE; + for (size_t poolIndex = 0; poolIndex < SOLIDSYSLOG_FILE_POOL_SIZE; poolIndex++) + { + if (base == &PlusFatFile_Pool[poolIndex].Base) + { + result = poolIndex; + break; + } + } + return result; +} + +static inline void PlusFatFile_CleanupAtIndex(size_t index, void* context) +{ + (void) context; + PlusFatFile_Cleanup(&PlusFatFile_Pool[index].Base); +} diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 56d3cc7d..7bcd1a08 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -16,6 +16,11 @@ if(DEFINED ENV{FATFS_PATH}) add_subdirectory(FatFs) endif() +if(DEFINED ENV{FREERTOS_PLUS_FAT_PATH}) + add_subdirectory(Support/PlusFatFakes) + add_subdirectory(PlusFat) +endif() + if(DEFINED ENV{LWIP_PATH}) add_subdirectory(Lwip) endif() @@ -263,6 +268,14 @@ foreach(fatfs_test_target IN ITEMS endif() endforeach() +foreach(plusfat_test_target IN ITEMS + SolidSyslogPlusFatFileTest +) + if(TARGET ${plusfat_test_target}) + register_junit_test(${plusfat_test_target}) + endif() +endforeach() + foreach(mbedtls_test_target IN ITEMS SolidSyslogMbedTlsStreamPoolTest SolidSyslogMbedTlsStreamTest diff --git a/Tests/PlusFat/CMakeLists.txt b/Tests/PlusFat/CMakeLists.txt new file mode 100644 index 00000000..4222eb07 --- /dev/null +++ b/Tests/PlusFat/CMakeLists.txt @@ -0,0 +1,50 @@ +# Tests/PlusFat — host-side unit tests for FreeRTOS-Plus-FAT adapters under +# Platform/PlusFat/Source/. Each test executable recompiles the adapter source +# under test with the PlusFatFakes config and links against the fakes — same +# shape as Tests/FatFs/ and Tests/FreeRtos/, see +# project_header_configured_platforms memory. +# +# Plus-FAT is FreeRTOS-coupled: ff_stdio.h pulls FreeRTOS.h / task.h / semphr.h, +# so the include path carries FreeRtosFakes/Interface (FreeRTOSConfig.h, +# portmacro.h) and $FREERTOS_KERNEL_PATH/include alongside the test-local +# FreeRTOSFATConfig.h in PlusFatFakes/Interface. We surface that coupling +# rather than hide it. +# +# Gated by $FREERTOS_PLUS_FAT_PATH at the top-level CMakeLists (this CMakeLists +# is only entered when FREERTOS_PLUS_FAT_PATH is set), and additionally needs +# FREERTOS_KERNEL_PATH for the kernel headers Plus-FAT depends on. + +if("$ENV{FREERTOS_KERNEL_PATH}" STREQUAL "") + message(STATUS "Skipping SolidSyslogPlusFatFileTest: FREERTOS_KERNEL_PATH not set.") +else() + add_executable(SolidSyslogPlusFatFileTest + SolidSyslogPlusFatFileTest.cpp + SolidSyslogPlusFatFilePoolTest.cpp + main.cpp + ${CMAKE_SOURCE_DIR}/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c + ${CMAKE_SOURCE_DIR}/Platform/PlusFat/Source/SolidSyslogPlusFatFileStatic.c + ${CMAKE_SOURCE_DIR}/Tests/Support/PlusFatFakes/Source/PlusFatFake.c + ) + + target_link_libraries(SolidSyslogPlusFatFileTest PRIVATE + ${PROJECT_NAME} + ErrorHandlerFake + ConfigLockFake + CppUTest + CppUTestExt + ) + + target_include_directories(SolidSyslogPlusFatFileTest PRIVATE + ${CMAKE_SOURCE_DIR}/Platform/PlusFat/Interface + ${CMAKE_SOURCE_DIR}/Core/Interface + ${CMAKE_SOURCE_DIR}/Core/Source + ${CMAKE_SOURCE_DIR}/Tests/Support + ${CMAKE_SOURCE_DIR}/Tests/Support/PlusFatFakes/Interface + ${CMAKE_SOURCE_DIR}/Tests/Support/PlusFatFakes/Source + ${CMAKE_SOURCE_DIR}/Tests/Support/FreeRtosFakes/Interface + $ENV{FREERTOS_KERNEL_PATH}/include + $ENV{FREERTOS_PLUS_FAT_PATH}/include + ) + + add_test(NAME SolidSyslogPlusFatFileTest COMMAND SolidSyslogPlusFatFileTest) +endif() diff --git a/Tests/PlusFat/SolidSyslogPlusFatFilePoolTest.cpp b/Tests/PlusFat/SolidSyslogPlusFatFilePoolTest.cpp new file mode 100644 index 00000000..d19b2e16 --- /dev/null +++ b/Tests/PlusFat/SolidSyslogPlusFatFilePoolTest.cpp @@ -0,0 +1,173 @@ +#include "CppUTest/TestHarness.h" +#include "TestUtils.h" + +extern "C" +{ +#include "ConfigLockFake.h" +#include "ErrorHandlerFake.h" +#include "PlusFatFake.h" +#include "SolidSyslogErrorCategory.h" +#include "SolidSyslogFile.h" +#include "SolidSyslogFileDefinition.h" +#include "SolidSyslogPlusFatFile.h" +#include "SolidSyslogPlusFatFileErrors.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogTunables.h" +} + +using namespace CososoTesting; + +// Asserts handle is non-null and not one of the slots in pool. +#define CHECK_IS_FALLBACK(handle, pool) \ + do \ + { \ + CHECK_TEXT((handle) != nullptr, "Fallback handle was nullptr"); \ + for (auto* slot : (pool)) \ + { \ + CHECK_TEXT(slot != nullptr, "pool slot was nullptr (FillPool failed?)"); \ + CHECK_TEXT((handle) != slot, "Fallback handle collided with a pool slot"); \ + } \ + } while (0) + +// clang-format off +TEST_GROUP(SolidSyslogPlusFatFilePool) +{ + struct SolidSyslogFile* pooled[SOLIDSYSLOG_FILE_POOL_SIZE] = {}; + struct SolidSyslogFile* overflow = nullptr; + + void setup() override + { + PlusFatFake_Reset(); + } + + void teardown() override + { + for (auto* handle : pooled) + { + if (handle != nullptr) + { + SolidSyslogPlusFatFile_Destroy(handle); + } + } + if (overflow != nullptr) + { + SolidSyslogPlusFatFile_Destroy(overflow); + } + ConfigLockFake_Uninstall(); + } + + void FillPool() + { + for (auto*& slot : pooled) + { + slot = SolidSyslogPlusFatFile_Create(); + } + } +}; + +// clang-format on + +TEST(SolidSyslogPlusFatFilePool, FillingPoolThenOverflowReturnsDistinctFallback) +{ + FillPool(); + + overflow = SolidSyslogPlusFatFile_Create(); + + CHECK_IS_FALLBACK(overflow, pooled); +} + +TEST(SolidSyslogPlusFatFilePool, ExhaustedCreateReportsError) +{ + ErrorHandlerFake_Install(nullptr); + FillPool(); + + overflow = SolidSyslogPlusFatFile_Create(); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, ErrorHandlerFake_LastSeverity()); + POINTERS_EQUAL(&PlusFatFileErrorSource, ErrorHandlerFake_LastSource()); + UNSIGNED_LONGS_EQUAL(SOLIDSYSLOG_CAT_POOL_EXHAUSTED, ErrorHandlerFake_LastCategory()); + UNSIGNED_LONGS_EQUAL(PLUSFATFILE_ERROR_POOL_EXHAUSTED, ErrorHandlerFake_LastDetail()); +} + +TEST(SolidSyslogPlusFatFilePool, FallbackOpenReturnsFalse) +{ + FillPool(); + overflow = SolidSyslogPlusFatFile_Create(); + + CHECK_FALSE(SolidSyslogFile_Open(overflow, "anything.log")); +} + +TEST(SolidSyslogPlusFatFilePool, CreateAcquiresAndReleasesConfigLockOnFirstFreeSlot) +{ + ConfigLockFake_Install(); + + pooled[0] = SolidSyslogPlusFatFile_Create(); + + CALLED_FAKE(ConfigLockFake_Lock, ONCE); + CALLED_FAKE(ConfigLockFake_Unlock, ONCE); +} + +TEST(SolidSyslogPlusFatFilePool, CreateLocksOncePerSlotProbedWhenPoolIsFull) +{ + FillPool(); + ConfigLockFake_Install(); + + overflow = SolidSyslogPlusFatFile_Create(); + + LONGS_EQUAL(SOLIDSYSLOG_FILE_POOL_SIZE, ConfigLockFake_LockCallCount()); + LONGS_EQUAL(SOLIDSYSLOG_FILE_POOL_SIZE, ConfigLockFake_UnlockCallCount()); +} + +TEST(SolidSyslogPlusFatFilePool, DestroyOfPooledHandleLocksOnce) +{ + pooled[0] = SolidSyslogPlusFatFile_Create(); + ConfigLockFake_Install(); + + SolidSyslogPlusFatFile_Destroy(pooled[0]); + pooled[0] = nullptr; + + CALLED_FAKE(ConfigLockFake_Lock, ONCE); + CALLED_FAKE(ConfigLockFake_Unlock, ONCE); +} + +TEST(SolidSyslogPlusFatFilePool, DestroyOfUnknownHandleDoesNotLock) +{ + ConfigLockFake_Install(); + struct SolidSyslogFile stranger = {}; + + SolidSyslogPlusFatFile_Destroy(&stranger); + + CALLED_FAKE(ConfigLockFake_Lock, NEVER); + CALLED_FAKE(ConfigLockFake_Unlock, NEVER); +} + +TEST(SolidSyslogPlusFatFilePool, DestroyOfUnknownHandleReportsWarning) +{ + ErrorHandlerFake_Install(nullptr); + struct SolidSyslogFile stranger = {}; + + SolidSyslogPlusFatFile_Destroy(&stranger); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity()); + POINTERS_EQUAL(&PlusFatFileErrorSource, ErrorHandlerFake_LastSource()); + UNSIGNED_LONGS_EQUAL(SOLIDSYSLOG_CAT_UNKNOWN_DESTROY, ErrorHandlerFake_LastCategory()); + UNSIGNED_LONGS_EQUAL(PLUSFATFILE_ERROR_UNKNOWN_DESTROY, ErrorHandlerFake_LastDetail()); +} + +TEST(SolidSyslogPlusFatFilePool, DestroyOfStaleHandleReportsWarning) +{ + pooled[0] = SolidSyslogPlusFatFile_Create(); + SolidSyslogPlusFatFile_Destroy(pooled[0]); + ErrorHandlerFake_Install(nullptr); + + SolidSyslogPlusFatFile_Destroy(pooled[0]); + pooled[0] = nullptr; + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity()); + POINTERS_EQUAL(&PlusFatFileErrorSource, ErrorHandlerFake_LastSource()); + UNSIGNED_LONGS_EQUAL(SOLIDSYSLOG_CAT_UNKNOWN_DESTROY, ErrorHandlerFake_LastCategory()); + UNSIGNED_LONGS_EQUAL(PLUSFATFILE_ERROR_UNKNOWN_DESTROY, ErrorHandlerFake_LastDetail()); +} diff --git a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp new file mode 100644 index 00000000..d449513a --- /dev/null +++ b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp @@ -0,0 +1,38 @@ +#include "CppUTest/TestHarness.h" +#include "TestUtils.h" + +extern "C" +{ +#include "PlusFatFake.h" +#include "SolidSyslogFile.h" +#include "SolidSyslogPlusFatFile.h" +#include "ff_stdio.h" +} + +using namespace CososoTesting; + +static const char* const TEST_PATH = "test.log"; + +// clang-format off +TEST_GROUP(SolidSyslogPlusFatFile) +{ + struct SolidSyslogFile* file = nullptr; + + void setup() override + { + PlusFatFake_Reset(); + file = SolidSyslogPlusFatFile_Create(); + } + + void teardown() override + { + SolidSyslogPlusFatFile_Destroy(file); + } +}; + +// clang-format on + +TEST(SolidSyslogPlusFatFile, CreateSucceeds) +{ + CHECK(file != nullptr); +} diff --git a/Tests/PlusFat/main.cpp b/Tests/PlusFat/main.cpp new file mode 100644 index 00000000..abc587a2 --- /dev/null +++ b/Tests/PlusFat/main.cpp @@ -0,0 +1,6 @@ +#include "CppUTest/CommandLineTestRunner.h" + +int main(int argc, char* argv[]) +{ + return CommandLineTestRunner::RunAllTests(argc, argv); +} diff --git a/Tests/Support/FreeRtosFakes/Interface/FreeRTOSConfig.h b/Tests/Support/FreeRtosFakes/Interface/FreeRTOSConfig.h index c58df8be..85fd2737 100644 --- a/Tests/Support/FreeRtosFakes/Interface/FreeRTOSConfig.h +++ b/Tests/Support/FreeRtosFakes/Interface/FreeRTOSConfig.h @@ -47,6 +47,14 @@ #define configUSE_CO_ROUTINES 0 #define configMAX_CO_ROUTINE_PRIORITIES 1 +/* Thread-local storage pointers. FreeRTOS-Plus-FAT's ff_stdio.h keeps three + * per-task entries (errno, CWD, ff_error) starting at ffconfigCWD_THREAD_LOCAL_INDEX + * and #errors at compile time if fewer than three are available; it also gates + * the v/pvTaskGetThreadLocalStoragePointer declarations on this being > 0. Set + * to 5 to clear that requirement with headroom. Harmless to the other host + * tests, which never touch thread-local storage. */ +#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5 + /* Optional API inclusions. */ #define INCLUDE_vTaskPrioritySet 0 #define INCLUDE_uxTaskPriorityGet 0 diff --git a/Tests/Support/PlusFatFakes/CMakeLists.txt b/Tests/Support/PlusFatFakes/CMakeLists.txt new file mode 100644 index 00000000..74c37049 --- /dev/null +++ b/Tests/Support/PlusFatFakes/CMakeLists.txt @@ -0,0 +1,15 @@ +# PlusFatFakes — host-side fake implementations of the FreeRTOS-Plus-FAT +# ff_stdio API used by Tests/PlusFat/*Test executables. The real Plus-FAT +# headers come from $FREERTOS_PLUS_FAT_PATH/include/; the test +# FreeRTOSFATConfig.h lives in this directory's Interface/. Drift between the +# fakes and the real Plus-FAT API is caught at compile time when the fake +# source includes ff_stdio.h. +# +# Plus-FAT is FreeRTOS-coupled (ff_headers.h pulls FreeRTOS.h / task.h / +# semphr.h), so each consuming test exe also puts FreeRtosFakes/Interface and +# $FREERTOS_KERNEL_PATH/include on its include path. We do not hide that +# coupling — it is intrinsic to Plus-FAT. +# +# The PlusFatFake.h control surface and PlusFatFake.c bodies grow per-slice as +# new ff_* calls enter the adapter. Each test exe pulls the fake source +# directly via add_executable — see Tests/FatFs/ and Tests/FreeRtos/ pattern. diff --git a/Tests/Support/PlusFatFakes/Interface/FreeRTOSFATConfig.h b/Tests/Support/PlusFatFakes/Interface/FreeRTOSFATConfig.h new file mode 100644 index 00000000..04582c08 --- /dev/null +++ b/Tests/Support/PlusFatFakes/Interface/FreeRTOSFATConfig.h @@ -0,0 +1,22 @@ +/* Host-test FreeRTOS-Plus-FAT configuration for Tests/PlusFat/. + * + * Plus-FAT is header-configured: ff_headers.h includes this file (after + * FreeRTOS.h) and FreeRTOSFATConfigDefaults.h fills in every value not set + * here. The three settings below are the minimum the defaults header demands + * — byte order, a thread-local-storage slot for the CWD, and a portINLINE + * fallback for the host compiler — matching Plus-FAT's own DefaultConf sample. + * + * Note: configNUM_THREAD_LOCAL_STORAGE_POINTERS (which ff_stdio.h requires to + * be >= 3) is a kernel setting and so lives in FreeRtosFakes/FreeRTOSConfig.h, + * which is included earlier via FreeRTOS.h. */ +#ifndef FREERTOSFATCONFIG_H +#define FREERTOSFATCONFIG_H + +#define ffconfigBYTE_ORDER ( pdFREERTOS_LITTLE_ENDIAN ) +#define ffconfigCWD_THREAD_LOCAL_INDEX ( 0 ) + +#if !defined( portINLINE ) + #define portINLINE __inline +#endif + +#endif /* FREERTOSFATCONFIG_H */ diff --git a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h new file mode 100644 index 00000000..1462c4dc --- /dev/null +++ b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h @@ -0,0 +1,12 @@ +#ifndef PLUSFATFAKE_H +#define PLUSFATFAKE_H + +#include "ExternC.h" + +EXTERN_C_BEGIN + + void PlusFatFake_Reset(void); + +EXTERN_C_END + +#endif /* PLUSFATFAKE_H */ diff --git a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c new file mode 100644 index 00000000..6c19b593 --- /dev/null +++ b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c @@ -0,0 +1,7 @@ +#include "PlusFatFake.h" + +#include "ff_stdio.h" + +void PlusFatFake_Reset(void) +{ +} From 4c6a37f65b799791172967d581f6ff8cc4ba1ee2 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 3 Jun 2026 21:39:20 +0100 Subject: [PATCH 2/8] feat: S29.04 PlusFat file Open/Close/IsOpen lifecycle Drive the SolidSyslogFile lifecycle ops on the PlusFat adapter via TDD: Open tries ff_fopen "r+" then falls back to "w+" (open-or-create with no truncation, since Plus-FAT has no open-always mode); Close calls ff_fclose guarded on an open handle; IsOpen and the open-state both derive from the FF_FILE* sentinel (no separate bool); Cleanup closes any open handle before the NullFile overwrite. PlusFatFake grows ff_fopen (with mode-fail injection and mode/path spies) and ff_fclose. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../PlusFat/Source/SolidSyslogPlusFatFile.c | 52 +++++++++++- .../Source/SolidSyslogPlusFatFilePrivate.h | 2 + Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp | 53 ++++++++++++ .../PlusFatFakes/Interface/PlusFatFake.h | 10 +++ .../Support/PlusFatFakes/Source/PlusFatFake.c | 81 +++++++++++++++++++ 5 files changed, 196 insertions(+), 2 deletions(-) diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c index 93bd86ed..4172da66 100644 --- a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c @@ -1,21 +1,69 @@ #include "SolidSyslogPlusFatFile.h" +#include +#include + #include "SolidSyslogFileDefinition.h" #include "SolidSyslogNullFile.h" #include "SolidSyslogPlusFatFileErrors.h" #include "SolidSyslogPlusFatFilePrivate.h" +#include "ff_stdio.h" const struct SolidSyslogErrorSource PlusFatFileErrorSource = {"PlusFatFile"}; +static bool PlusFatFile_Open(struct SolidSyslogFile* base, const char* path); +static void PlusFatFile_Close(struct SolidSyslogFile* base); +static bool PlusFatFile_IsOpen(struct SolidSyslogFile* base); + +static inline struct SolidSyslogPlusFatFile* PlusFatFile_SelfFromBase(struct SolidSyslogFile* base); + void PlusFatFile_Initialise(struct SolidSyslogFile* base) { - /* Vtable wiring is grown by the behavioural TDD slices. */ - (void) base; + struct SolidSyslogPlusFatFile* self = PlusFatFile_SelfFromBase(base); + self->Base.Open = PlusFatFile_Open; + self->Base.Close = PlusFatFile_Close; + self->Base.IsOpen = PlusFatFile_IsOpen; + self->Fp = NULL; +} + +static inline struct SolidSyslogPlusFatFile* PlusFatFile_SelfFromBase(struct SolidSyslogFile* base) +{ + return (struct SolidSyslogPlusFatFile*) base; } void PlusFatFile_Cleanup(struct SolidSyslogFile* base) { + PlusFatFile_Close(base); /* Overwrite the abstract base with the shared NullFile vtable so * use-after-destroy is a safe no-op rather than a NULL-fn-pointer crash. */ *base = *SolidSyslogNullFile_Get(); } + +static bool PlusFatFile_Open(struct SolidSyslogFile* base, const char* path) +{ + struct SolidSyslogPlusFatFile* self = PlusFatFile_SelfFromBase(base); + /* "r+" opens an existing file without truncating; if it does not exist, + * "w+" creates it. Together this is open-or-create with no data loss — + * Plus-FAT has no single open-always mode. */ + self->Fp = ff_fopen(path, "r+"); + if (self->Fp == NULL) + { + self->Fp = ff_fopen(path, "w+"); + } + return self->Fp != NULL; +} + +static void PlusFatFile_Close(struct SolidSyslogFile* base) +{ + struct SolidSyslogPlusFatFile* self = PlusFatFile_SelfFromBase(base); + if (self->Fp != NULL) + { + ff_fclose(self->Fp); + self->Fp = NULL; + } +} + +static bool PlusFatFile_IsOpen(struct SolidSyslogFile* base) +{ + return PlusFatFile_SelfFromBase(base)->Fp != NULL; +} diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFilePrivate.h b/Platform/PlusFat/Source/SolidSyslogPlusFatFilePrivate.h index d4686f8d..d4b339f6 100644 --- a/Platform/PlusFat/Source/SolidSyslogPlusFatFilePrivate.h +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFilePrivate.h @@ -7,10 +7,12 @@ #include "SolidSyslogFileDefinition.h" #include "SolidSyslogPlusFatFileErrors.h" #include "SolidSyslogPrival.h" +#include "ff_stdio.h" struct SolidSyslogPlusFatFile { struct SolidSyslogFile Base; + FF_FILE* Fp; }; void PlusFatFile_Initialise(struct SolidSyslogFile* base); diff --git a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp index d449513a..d0399de4 100644 --- a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp +++ b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp @@ -36,3 +36,56 @@ TEST(SolidSyslogPlusFatFile, CreateSucceeds) { CHECK(file != nullptr); } + +TEST(SolidSyslogPlusFatFile, OpenSucceeds) +{ + CHECK_TRUE(SolidSyslogFile_Open(file, TEST_PATH)); + CHECK_TRUE(SolidSyslogFile_IsOpen(file)); +} + +TEST(SolidSyslogPlusFatFile, OpenFallsBackToCreateModeWhenReadPlusFails) +{ + PlusFatFake_SetOpenFailsForMode("r+"); + + CHECK_TRUE(SolidSyslogFile_Open(file, TEST_PATH)); + + LONGS_EQUAL(2, PlusFatFake_OpenCallCount()); + STRCMP_EQUAL(TEST_PATH, PlusFatFake_LastOpenPath()); + STRCMP_EQUAL("r+", PlusFatFake_OpenModeAt(0)); + STRCMP_EQUAL("w+", PlusFatFake_OpenModeAt(1)); +} + +TEST(SolidSyslogPlusFatFile, OpenFailsWhenBothModesFail) +{ + PlusFatFake_SetOpenAlwaysFails(); + + CHECK_FALSE(SolidSyslogFile_Open(file, TEST_PATH)); + CHECK_FALSE(SolidSyslogFile_IsOpen(file)); +} + +TEST(SolidSyslogPlusFatFile, CloseCallsFfcloseAndClearsIsOpen) +{ + SolidSyslogFile_Open(file, TEST_PATH); + + SolidSyslogFile_Close(file); + + CALLED_FAKE(PlusFatFake_Close, ONCE); + CHECK_FALSE(SolidSyslogFile_IsOpen(file)); +} + +TEST(SolidSyslogPlusFatFile, CloseIsNoOpWhenAlreadyClosed) +{ + SolidSyslogFile_Close(file); + + CALLED_FAKE(PlusFatFake_Close, NEVER); +} + +TEST(SolidSyslogPlusFatFile, DestroyClosesOpenFile) +{ + SolidSyslogFile_Open(file, TEST_PATH); + + SolidSyslogPlusFatFile_Destroy(file); + file = nullptr; + + CALLED_FAKE(PlusFatFake_Close, ONCE); +} diff --git a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h index 1462c4dc..52361100 100644 --- a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h +++ b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h @@ -7,6 +7,16 @@ EXTERN_C_BEGIN void PlusFatFake_Reset(void); + /* ff_fopen */ + void PlusFatFake_SetOpenFailsForMode(const char* mode); + void PlusFatFake_SetOpenAlwaysFails(void); + int PlusFatFake_OpenCallCount(void); + const char* PlusFatFake_OpenModeAt(int index); + const char* PlusFatFake_LastOpenPath(void); + + /* ff_fclose */ + int PlusFatFake_CloseCallCount(void); + EXTERN_C_END #endif /* PLUSFATFAKE_H */ diff --git a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c index 6c19b593..0a1ac9b1 100644 --- a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c +++ b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c @@ -1,7 +1,88 @@ #include "PlusFatFake.h" +#include +#include +#include + #include "ff_stdio.h" +enum +{ + OPEN_MODE_CAPACITY = 8 +}; + +static FF_FILE fakeFile; + +/* ff_fopen state */ +static int openCallCount; +static const char* openModes[OPEN_MODE_CAPACITY]; +static const char* lastOpenPath; +static const char* openFailMode; +static bool openAlwaysFails; + +/* ff_fclose state */ +static int closeCallCount; + void PlusFatFake_Reset(void) { + openCallCount = 0; + lastOpenPath = NULL; + openFailMode = NULL; + openAlwaysFails = false; + memset(openModes, 0, sizeof(openModes)); + closeCallCount = 0; +} + +void PlusFatFake_SetOpenFailsForMode(const char* mode) +{ + openFailMode = mode; +} + +void PlusFatFake_SetOpenAlwaysFails(void) +{ + openAlwaysFails = true; +} + +int PlusFatFake_OpenCallCount(void) +{ + return openCallCount; +} + +const char* PlusFatFake_OpenModeAt(int index) +{ + return openModes[index]; +} + +const char* PlusFatFake_LastOpenPath(void) +{ + return lastOpenPath; +} + +FF_FILE* ff_fopen(const char* pcFile, const char* pcMode) +{ + if (openCallCount < OPEN_MODE_CAPACITY) + { + openModes[openCallCount] = pcMode; + } + lastOpenPath = pcFile; + openCallCount++; + + FF_FILE* result = &fakeFile; + if (openAlwaysFails || ((openFailMode != NULL) && (strcmp(pcMode, openFailMode) == 0))) + { + result = NULL; + } + return result; +} + +int PlusFatFake_CloseCallCount(void) +{ + return closeCallCount; +} + +int ff_fclose(FF_FILE* pxStream) +{ + (void) pxStream; + closeCallCount++; + return 0; } From f8442a3d51dc675b07a5e0b20c58e6b85dd3f979 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 3 Jun 2026 21:43:00 +0100 Subject: [PATCH 3/8] feat: S29.04 PlusFat file Read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read maps to ff_fread(buf, 1, count, fp) and succeeds only when the returned item count equals the requested count — a short read is the single failure mode (Plus-FAT's ff_fread has no separate status return). PlusFatFake grows ff_fread with source injection and size/items/call-count spies. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../PlusFat/Source/SolidSyslogPlusFatFile.c | 9 ++++ Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp | 34 +++++++++++++ .../PlusFatFakes/Interface/PlusFatFake.h | 6 +++ .../Support/PlusFatFakes/Source/PlusFatFake.c | 51 ++++++++++++++++++- 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c index 4172da66..f51cf826 100644 --- a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c @@ -14,6 +14,7 @@ const struct SolidSyslogErrorSource PlusFatFileErrorSource = {"PlusFatFile"}; static bool PlusFatFile_Open(struct SolidSyslogFile* base, const char* path); static void PlusFatFile_Close(struct SolidSyslogFile* base); static bool PlusFatFile_IsOpen(struct SolidSyslogFile* base); +static bool PlusFatFile_Read(struct SolidSyslogFile* base, void* buf, size_t count); static inline struct SolidSyslogPlusFatFile* PlusFatFile_SelfFromBase(struct SolidSyslogFile* base); @@ -23,6 +24,7 @@ void PlusFatFile_Initialise(struct SolidSyslogFile* base) self->Base.Open = PlusFatFile_Open; self->Base.Close = PlusFatFile_Close; self->Base.IsOpen = PlusFatFile_IsOpen; + self->Base.Read = PlusFatFile_Read; self->Fp = NULL; } @@ -67,3 +69,10 @@ static bool PlusFatFile_IsOpen(struct SolidSyslogFile* base) { return PlusFatFile_SelfFromBase(base)->Fp != NULL; } + +static bool PlusFatFile_Read(struct SolidSyslogFile* base, void* buf, size_t count) +{ + struct SolidSyslogPlusFatFile* self = PlusFatFile_SelfFromBase(base); + /* xSize == 1 so the ff_fread return value is the byte count delivered. */ + return ff_fread(buf, 1, count, self->Fp) == count; +} diff --git a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp index d0399de4..da339db3 100644 --- a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp +++ b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp @@ -17,6 +17,7 @@ static const char* const TEST_PATH = "test.log"; TEST_GROUP(SolidSyslogPlusFatFile) { struct SolidSyslogFile* file = nullptr; + char buffer[5] = {}; void setup() override { @@ -89,3 +90,36 @@ TEST(SolidSyslogPlusFatFile, DestroyClosesOpenFile) CALLED_FAKE(PlusFatFake_Close, ONCE); } + +TEST(SolidSyslogPlusFatFile, ReadReturnsRequestedBytes) +{ + SolidSyslogFile_Open(file, TEST_PATH); + const char source[5] = {'h', 'e', 'l', 'l', 'o'}; + PlusFatFake_SetReadSource(source, sizeof(source)); + + CHECK_TRUE(SolidSyslogFile_Read(file, buffer, sizeof(buffer))); + + MEMCMP_EQUAL(source, buffer, sizeof(source)); +} + +TEST(SolidSyslogPlusFatFile, ReadCallsFfreadWithUnitSizeAndCount) +{ + SolidSyslogFile_Open(file, TEST_PATH); + const char source[5] = {'h', 'e', 'l', 'l', 'o'}; + PlusFatFake_SetReadSource(source, sizeof(source)); + + SolidSyslogFile_Read(file, buffer, sizeof(buffer)); + + CALLED_FAKE(PlusFatFake_Read, ONCE); + UNSIGNED_LONGS_EQUAL(1, PlusFatFake_LastReadSize()); + UNSIGNED_LONGS_EQUAL(sizeof(buffer), PlusFatFake_LastReadItems()); +} + +TEST(SolidSyslogPlusFatFile, ReadFailsWhenFewerBytesAvailable) +{ + SolidSyslogFile_Open(file, TEST_PATH); + const char source[3] = {'a', 'b', 'c'}; + PlusFatFake_SetReadSource(source, sizeof(source)); + + CHECK_FALSE(SolidSyslogFile_Read(file, buffer, sizeof(buffer))); +} diff --git a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h index 52361100..f73bd2ac 100644 --- a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h +++ b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h @@ -17,6 +17,12 @@ EXTERN_C_BEGIN /* ff_fclose */ int PlusFatFake_CloseCallCount(void); + /* ff_fread */ + void PlusFatFake_SetReadSource(const void* bytes, unsigned long count); + int PlusFatFake_ReadCallCount(void); + unsigned long PlusFatFake_LastReadSize(void); + unsigned long PlusFatFake_LastReadItems(void); + EXTERN_C_END #endif /* PLUSFATFAKE_H */ diff --git a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c index 0a1ac9b1..23685c89 100644 --- a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c +++ b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c @@ -8,7 +8,8 @@ enum { - OPEN_MODE_CAPACITY = 8 + OPEN_MODE_CAPACITY = 8, + READ_SOURCE_CAPACITY = 256 }; static FF_FILE fakeFile; @@ -23,6 +24,13 @@ static bool openAlwaysFails; /* ff_fclose state */ static int closeCallCount; +/* ff_fread state */ +static unsigned char readSource[READ_SOURCE_CAPACITY]; +static size_t readSourceCount; +static int readCallCount; +static size_t lastReadSize; +static size_t lastReadItems; + void PlusFatFake_Reset(void) { openCallCount = 0; @@ -31,6 +39,11 @@ void PlusFatFake_Reset(void) openAlwaysFails = false; memset(openModes, 0, sizeof(openModes)); closeCallCount = 0; + memset(readSource, 0, sizeof(readSource)); + readSourceCount = 0; + readCallCount = 0; + lastReadSize = 0; + lastReadItems = 0; } void PlusFatFake_SetOpenFailsForMode(const char* mode) @@ -86,3 +99,39 @@ int ff_fclose(FF_FILE* pxStream) closeCallCount++; return 0; } + +void PlusFatFake_SetReadSource(const void* bytes, unsigned long count) +{ + size_t copyCount = (count <= sizeof(readSource)) ? (size_t) count : sizeof(readSource); + memcpy(readSource, bytes, copyCount); + readSourceCount = copyCount; +} + +/* The adapter always calls ff_fread with xSize == 1, so item count == byte + * count. Copies from the programmed source and returns the number of items + * (bytes) delivered, capped at the programmed source length. */ +int PlusFatFake_ReadCallCount(void) +{ + return readCallCount; +} + +unsigned long PlusFatFake_LastReadSize(void) +{ + return (unsigned long) lastReadSize; +} + +unsigned long PlusFatFake_LastReadItems(void) +{ + return (unsigned long) lastReadItems; +} + +size_t ff_fread(void* pvBuffer, size_t xSize, size_t xItems, FF_FILE* pxStream) +{ + (void) pxStream; + readCallCount++; + lastReadSize = xSize; + lastReadItems = xItems; + size_t itemsDelivered = (xItems <= readSourceCount) ? xItems : readSourceCount; + memcpy(pvBuffer, readSource, itemsDelivered); + return itemsDelivered; +} From 85fd33e4092e5531bd6e48ab14e9b903fd1d139b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 3 Jun 2026 21:49:00 +0100 Subject: [PATCH 4/8] feat: S29.04 PlusFat file Write with per-write durability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Write maps to ff_fwrite(buf, 1, count, fp) and, only on a complete write, ff_fflush — so a power loss never loses a record the BlockStore was told had been stored. Fails on a short write (skipping the flush) or a flush error. PlusFatFake grows ff_fwrite (data/items spies + incomplete-write injection) and ff_fflush (call-count spy + fail injection). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../PlusFat/Source/SolidSyslogPlusFatFile.c | 11 +++ Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp | 38 ++++++++++ .../PlusFatFakes/Interface/PlusFatFake.h | 10 +++ .../Support/PlusFatFakes/Source/PlusFatFake.c | 72 ++++++++++++++++++- 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c index f51cf826..dea63129 100644 --- a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c @@ -15,6 +15,7 @@ static bool PlusFatFile_Open(struct SolidSyslogFile* base, const char* path); static void PlusFatFile_Close(struct SolidSyslogFile* base); static bool PlusFatFile_IsOpen(struct SolidSyslogFile* base); static bool PlusFatFile_Read(struct SolidSyslogFile* base, void* buf, size_t count); +static bool PlusFatFile_Write(struct SolidSyslogFile* base, const void* buf, size_t count); static inline struct SolidSyslogPlusFatFile* PlusFatFile_SelfFromBase(struct SolidSyslogFile* base); @@ -25,6 +26,7 @@ void PlusFatFile_Initialise(struct SolidSyslogFile* base) self->Base.Close = PlusFatFile_Close; self->Base.IsOpen = PlusFatFile_IsOpen; self->Base.Read = PlusFatFile_Read; + self->Base.Write = PlusFatFile_Write; self->Fp = NULL; } @@ -76,3 +78,12 @@ static bool PlusFatFile_Read(struct SolidSyslogFile* base, void* buf, size_t cou /* xSize == 1 so the ff_fread return value is the byte count delivered. */ return ff_fread(buf, 1, count, self->Fp) == count; } + +static bool PlusFatFile_Write(struct SolidSyslogFile* base, const void* buf, size_t count) +{ + struct SolidSyslogPlusFatFile* self = PlusFatFile_SelfFromBase(base); + /* Flush after every complete write so a power loss never loses a record + * the BlockStore was told had been stored. */ + bool wroteAll = ff_fwrite(buf, 1, count, self->Fp) == count; + return wroteAll && (ff_fflush(self->Fp) == 0); +} diff --git a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp index da339db3..c4bcb629 100644 --- a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp +++ b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp @@ -123,3 +123,41 @@ TEST(SolidSyslogPlusFatFile, ReadFailsWhenFewerBytesAvailable) CHECK_FALSE(SolidSyslogFile_Read(file, buffer, sizeof(buffer))); } + +TEST(SolidSyslogPlusFatFile, WriteCallsFfwriteWithData) +{ + SolidSyslogFile_Open(file, TEST_PATH); + + CHECK_TRUE(SolidSyslogFile_Write(file, buffer, sizeof(buffer))); + + CALLED_FAKE(PlusFatFake_Write, ONCE); + MEMCMP_EQUAL(buffer, PlusFatFake_LastWriteBytes(), sizeof(buffer)); + UNSIGNED_LONGS_EQUAL(sizeof(buffer), PlusFatFake_LastWriteItems()); +} + +TEST(SolidSyslogPlusFatFile, WriteCommitsToDiskWithFfflush) +{ + SolidSyslogFile_Open(file, TEST_PATH); + + SolidSyslogFile_Write(file, buffer, 1); + + CALLED_FAKE(PlusFatFake_Fflush, ONCE); +} + +TEST(SolidSyslogPlusFatFile, WriteFailsAndSkipsFlushWhenFfwriteIncomplete) +{ + SolidSyslogFile_Open(file, TEST_PATH); + PlusFatFake_SetWriteIncomplete(); + + CHECK_FALSE(SolidSyslogFile_Write(file, buffer, sizeof(buffer))); + + CALLED_FAKE(PlusFatFake_Fflush, NEVER); +} + +TEST(SolidSyslogPlusFatFile, WriteFailsWhenFfflushFails) +{ + SolidSyslogFile_Open(file, TEST_PATH); + PlusFatFake_SetFflushFails(); + + CHECK_FALSE(SolidSyslogFile_Write(file, buffer, sizeof(buffer))); +} diff --git a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h index f73bd2ac..aa4e3f72 100644 --- a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h +++ b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h @@ -23,6 +23,16 @@ EXTERN_C_BEGIN unsigned long PlusFatFake_LastReadSize(void); unsigned long PlusFatFake_LastReadItems(void); + /* ff_fwrite */ + void PlusFatFake_SetWriteIncomplete(void); + int PlusFatFake_WriteCallCount(void); + const void* PlusFatFake_LastWriteBytes(void); + unsigned long PlusFatFake_LastWriteItems(void); + + /* ff_fflush */ + void PlusFatFake_SetFflushFails(void); + int PlusFatFake_FflushCallCount(void); + EXTERN_C_END #endif /* PLUSFATFAKE_H */ diff --git a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c index 23685c89..bf1cd9d4 100644 --- a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c +++ b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c @@ -9,7 +9,8 @@ enum { OPEN_MODE_CAPACITY = 8, - READ_SOURCE_CAPACITY = 256 + READ_SOURCE_CAPACITY = 256, + WRITE_CAPTURE_CAPACITY = 256 }; static FF_FILE fakeFile; @@ -31,6 +32,16 @@ static int readCallCount; static size_t lastReadSize; static size_t lastReadItems; +/* ff_fwrite state */ +static int writeCallCount; +static unsigned char lastWriteBytes[WRITE_CAPTURE_CAPACITY]; +static size_t lastWriteItems; +static bool writeIncomplete; + +/* ff_fflush state */ +static int fflushCallCount; +static int fflushResult; + void PlusFatFake_Reset(void) { openCallCount = 0; @@ -44,6 +55,12 @@ void PlusFatFake_Reset(void) readCallCount = 0; lastReadSize = 0; lastReadItems = 0; + writeCallCount = 0; + memset(lastWriteBytes, 0, sizeof(lastWriteBytes)); + lastWriteItems = 0; + writeIncomplete = false; + fflushCallCount = 0; + fflushResult = 0; } void PlusFatFake_SetOpenFailsForMode(const char* mode) @@ -135,3 +152,56 @@ size_t ff_fread(void* pvBuffer, size_t xSize, size_t xItems, FF_FILE* pxStream) memcpy(pvBuffer, readSource, itemsDelivered); return itemsDelivered; } + +void PlusFatFake_SetWriteIncomplete(void) +{ + writeIncomplete = true; +} + +int PlusFatFake_WriteCallCount(void) +{ + return writeCallCount; +} + +const void* PlusFatFake_LastWriteBytes(void) +{ + return lastWriteBytes; +} + +unsigned long PlusFatFake_LastWriteItems(void) +{ + return (unsigned long) lastWriteItems; +} + +size_t ff_fwrite(const void* pvBuffer, size_t xSize, size_t xItems, FF_FILE* pxStream) +{ + (void) xSize; + (void) pxStream; + writeCallCount++; + size_t copyCount = (xItems <= sizeof(lastWriteBytes)) ? xItems : sizeof(lastWriteBytes); + memcpy(lastWriteBytes, pvBuffer, copyCount); + lastWriteItems = xItems; + size_t itemsWritten = xItems; + if (writeIncomplete && (xItems > 0)) + { + itemsWritten = xItems - 1; + } + return itemsWritten; +} + +void PlusFatFake_SetFflushFails(void) +{ + fflushResult = -1; +} + +int PlusFatFake_FflushCallCount(void) +{ + return fflushCallCount; +} + +int ff_fflush(FF_FILE* pxStream) +{ + (void) pxStream; + fflushCallCount++; + return fflushResult; +} From 37b2f31a7db8e29ad8e542ac8476a65a85f0112d Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 3 Jun 2026 21:52:28 +0100 Subject: [PATCH 5/8] feat: S29.04 PlusFat file SeekTo and Size SeekTo maps to ff_fseek(fp, offset, SEEK_SET); Size returns ff_filelength. PlusFatFake grows ff_fseek (offset/whence spies) and ff_filelength (length injection). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../PlusFat/Source/SolidSyslogPlusFatFile.c | 15 ++++++ Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp | 19 ++++++++ .../PlusFatFakes/Interface/PlusFatFake.h | 8 ++++ .../Support/PlusFatFakes/Source/PlusFatFake.c | 47 +++++++++++++++++++ 4 files changed, 89 insertions(+) diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c index dea63129..f201c21e 100644 --- a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c @@ -16,6 +16,8 @@ static void PlusFatFile_Close(struct SolidSyslogFile* base); static bool PlusFatFile_IsOpen(struct SolidSyslogFile* base); static bool PlusFatFile_Read(struct SolidSyslogFile* base, void* buf, size_t count); static bool PlusFatFile_Write(struct SolidSyslogFile* base, const void* buf, size_t count); +static void PlusFatFile_SeekTo(struct SolidSyslogFile* base, size_t offset); +static size_t PlusFatFile_Size(struct SolidSyslogFile* base); static inline struct SolidSyslogPlusFatFile* PlusFatFile_SelfFromBase(struct SolidSyslogFile* base); @@ -27,6 +29,8 @@ void PlusFatFile_Initialise(struct SolidSyslogFile* base) self->Base.IsOpen = PlusFatFile_IsOpen; self->Base.Read = PlusFatFile_Read; self->Base.Write = PlusFatFile_Write; + self->Base.SeekTo = PlusFatFile_SeekTo; + self->Base.Size = PlusFatFile_Size; self->Fp = NULL; } @@ -87,3 +91,14 @@ static bool PlusFatFile_Write(struct SolidSyslogFile* base, const void* buf, siz bool wroteAll = ff_fwrite(buf, 1, count, self->Fp) == count; return wroteAll && (ff_fflush(self->Fp) == 0); } + +static void PlusFatFile_SeekTo(struct SolidSyslogFile* base, size_t offset) +{ + struct SolidSyslogPlusFatFile* self = PlusFatFile_SelfFromBase(base); + ff_fseek(self->Fp, (long) offset, SEEK_SET); +} + +static size_t PlusFatFile_Size(struct SolidSyslogFile* base) +{ + return ff_filelength(PlusFatFile_SelfFromBase(base)->Fp); +} diff --git a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp index c4bcb629..4a1188e8 100644 --- a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp +++ b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp @@ -161,3 +161,22 @@ TEST(SolidSyslogPlusFatFile, WriteFailsWhenFfflushFails) CHECK_FALSE(SolidSyslogFile_Write(file, buffer, sizeof(buffer))); } + +TEST(SolidSyslogPlusFatFile, SeekToCallsFfseekFromStart) +{ + SolidSyslogFile_Open(file, TEST_PATH); + + SolidSyslogFile_SeekTo(file, 42); + + CALLED_FAKE(PlusFatFake_Seek, ONCE); + LONGS_EQUAL(42, PlusFatFake_LastSeekOffset()); + LONGS_EQUAL(SEEK_SET, PlusFatFake_LastSeekWhence()); +} + +TEST(SolidSyslogPlusFatFile, SizeReturnsFfilelength) +{ + SolidSyslogFile_Open(file, TEST_PATH); + PlusFatFake_SetFileLength(42); + + LONGS_EQUAL(42, SolidSyslogFile_Size(file)); +} diff --git a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h index aa4e3f72..10a56db2 100644 --- a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h +++ b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h @@ -33,6 +33,14 @@ EXTERN_C_BEGIN void PlusFatFake_SetFflushFails(void); int PlusFatFake_FflushCallCount(void); + /* ff_fseek */ + int PlusFatFake_SeekCallCount(void); + long PlusFatFake_LastSeekOffset(void); + int PlusFatFake_LastSeekWhence(void); + + /* ff_filelength */ + void PlusFatFake_SetFileLength(unsigned long length); + EXTERN_C_END #endif /* PLUSFATFAKE_H */ diff --git a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c index bf1cd9d4..3a9d8a5a 100644 --- a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c +++ b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c @@ -42,6 +42,14 @@ static bool writeIncomplete; static int fflushCallCount; static int fflushResult; +/* ff_fseek state */ +static int seekCallCount; +static long lastSeekOffset; +static int lastSeekWhence; + +/* ff_filelength state */ +static size_t fileLength; + void PlusFatFake_Reset(void) { openCallCount = 0; @@ -61,6 +69,10 @@ void PlusFatFake_Reset(void) writeIncomplete = false; fflushCallCount = 0; fflushResult = 0; + seekCallCount = 0; + lastSeekOffset = 0; + lastSeekWhence = 0; + fileLength = 0; } void PlusFatFake_SetOpenFailsForMode(const char* mode) @@ -205,3 +217,38 @@ int ff_fflush(FF_FILE* pxStream) fflushCallCount++; return fflushResult; } + +int PlusFatFake_SeekCallCount(void) +{ + return seekCallCount; +} + +long PlusFatFake_LastSeekOffset(void) +{ + return lastSeekOffset; +} + +int PlusFatFake_LastSeekWhence(void) +{ + return lastSeekWhence; +} + +int ff_fseek(FF_FILE* pxStream, long lOffset, int iWhence) +{ + (void) pxStream; + seekCallCount++; + lastSeekOffset = lOffset; + lastSeekWhence = iWhence; + return 0; +} + +void PlusFatFake_SetFileLength(unsigned long length) +{ + fileLength = (size_t) length; +} + +size_t ff_filelength(FF_FILE* pxStream) +{ + (void) pxStream; + return fileLength; +} From b3345e09aa8207262f2a4dfee199a0314425bd73 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 3 Jun 2026 21:54:19 +0100 Subject: [PATCH 6/8] feat: S29.04 PlusFat file Truncate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Truncate empties the file via ff_fseek(fp, 0, SEEK_SET) then ff_seteof — Plus-FAT has no truncate-to-zero call of its own. PlusFatFake grows ff_seteof. Co-Authored-By: Claude Opus 4.8 (1M context) --- Platform/PlusFat/Source/SolidSyslogPlusFatFile.c | 12 ++++++++++++ Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp | 11 +++++++++++ .../Support/PlusFatFakes/Interface/PlusFatFake.h | 3 +++ Tests/Support/PlusFatFakes/Source/PlusFatFake.c | 16 ++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c index f201c21e..dcefca1b 100644 --- a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c @@ -18,6 +18,7 @@ static bool PlusFatFile_Read(struct SolidSyslogFile* base, void* buf, size_t cou static bool PlusFatFile_Write(struct SolidSyslogFile* base, const void* buf, size_t count); static void PlusFatFile_SeekTo(struct SolidSyslogFile* base, size_t offset); static size_t PlusFatFile_Size(struct SolidSyslogFile* base); +static void PlusFatFile_Truncate(struct SolidSyslogFile* base); static inline struct SolidSyslogPlusFatFile* PlusFatFile_SelfFromBase(struct SolidSyslogFile* base); @@ -31,6 +32,7 @@ void PlusFatFile_Initialise(struct SolidSyslogFile* base) self->Base.Write = PlusFatFile_Write; self->Base.SeekTo = PlusFatFile_SeekTo; self->Base.Size = PlusFatFile_Size; + self->Base.Truncate = PlusFatFile_Truncate; self->Fp = NULL; } @@ -102,3 +104,13 @@ static size_t PlusFatFile_Size(struct SolidSyslogFile* base) { return ff_filelength(PlusFatFile_SelfFromBase(base)->Fp); } + +static void PlusFatFile_Truncate(struct SolidSyslogFile* base) +{ + struct SolidSyslogPlusFatFile* self = PlusFatFile_SelfFromBase(base); + /* Empty the file: rewind to the start, then set EOF there so the length + * becomes zero. ff_seteof truncates at the current position and Plus-FAT + * has no truncate-to-zero call of its own. */ + ff_fseek(self->Fp, 0, SEEK_SET); + ff_seteof(self->Fp); +} diff --git a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp index 4a1188e8..a90bba75 100644 --- a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp +++ b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp @@ -180,3 +180,14 @@ TEST(SolidSyslogPlusFatFile, SizeReturnsFfilelength) LONGS_EQUAL(42, SolidSyslogFile_Size(file)); } + +TEST(SolidSyslogPlusFatFile, TruncateSeeksToZeroAndCallsFseteof) +{ + SolidSyslogFile_Open(file, TEST_PATH); + + SolidSyslogFile_Truncate(file); + + CALLED_FAKE(PlusFatFake_Seek, ONCE); + LONGS_EQUAL(0, PlusFatFake_LastSeekOffset()); + CALLED_FAKE(PlusFatFake_Seteof, ONCE); +} diff --git a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h index 10a56db2..e55d7aa7 100644 --- a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h +++ b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h @@ -41,6 +41,9 @@ EXTERN_C_BEGIN /* ff_filelength */ void PlusFatFake_SetFileLength(unsigned long length); + /* ff_seteof */ + int PlusFatFake_SeteofCallCount(void); + EXTERN_C_END #endif /* PLUSFATFAKE_H */ diff --git a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c index 3a9d8a5a..e58a0fc0 100644 --- a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c +++ b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c @@ -50,6 +50,9 @@ static int lastSeekWhence; /* ff_filelength state */ static size_t fileLength; +/* ff_seteof state */ +static int seteofCallCount; + void PlusFatFake_Reset(void) { openCallCount = 0; @@ -73,6 +76,7 @@ void PlusFatFake_Reset(void) lastSeekOffset = 0; lastSeekWhence = 0; fileLength = 0; + seteofCallCount = 0; } void PlusFatFake_SetOpenFailsForMode(const char* mode) @@ -252,3 +256,15 @@ size_t ff_filelength(FF_FILE* pxStream) (void) pxStream; return fileLength; } + +int PlusFatFake_SeteofCallCount(void) +{ + return seteofCallCount; +} + +int ff_seteof(FF_FILE* pxStream) +{ + (void) pxStream; + seteofCallCount++; + return 0; +} From 09bc04d20ab53bfe5358c5a9d62f7b72c3a1049a Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 3 Jun 2026 21:57:23 +0100 Subject: [PATCH 7/8] feat: S29.04 PlusFat file Exists and Delete Exists maps to ff_stat (true when it returns 0); Delete maps to ff_remove (true when it returns 0). Both ignore the file handle and act on the path. PlusFatFake grows ff_stat and ff_remove with path spies and fail injection. This completes the SolidSyslogFile vtable for the PlusFat adapter. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../PlusFat/Source/SolidSyslogPlusFatFile.c | 17 ++++++ Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp | 30 +++++++++ .../PlusFatFakes/Interface/PlusFatFake.h | 10 +++ .../Support/PlusFatFakes/Source/PlusFatFake.c | 61 +++++++++++++++++++ 4 files changed, 118 insertions(+) diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c index dcefca1b..04b550f1 100644 --- a/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFile.c @@ -19,6 +19,8 @@ static bool PlusFatFile_Write(struct SolidSyslogFile* base, const void* buf, siz static void PlusFatFile_SeekTo(struct SolidSyslogFile* base, size_t offset); static size_t PlusFatFile_Size(struct SolidSyslogFile* base); static void PlusFatFile_Truncate(struct SolidSyslogFile* base); +static bool PlusFatFile_Exists(struct SolidSyslogFile* base, const char* path); +static bool PlusFatFile_Delete(struct SolidSyslogFile* base, const char* path); static inline struct SolidSyslogPlusFatFile* PlusFatFile_SelfFromBase(struct SolidSyslogFile* base); @@ -33,6 +35,8 @@ void PlusFatFile_Initialise(struct SolidSyslogFile* base) self->Base.SeekTo = PlusFatFile_SeekTo; self->Base.Size = PlusFatFile_Size; self->Base.Truncate = PlusFatFile_Truncate; + self->Base.Exists = PlusFatFile_Exists; + self->Base.Delete = PlusFatFile_Delete; self->Fp = NULL; } @@ -114,3 +118,16 @@ static void PlusFatFile_Truncate(struct SolidSyslogFile* base) ff_fseek(self->Fp, 0, SEEK_SET); ff_seteof(self->Fp); } + +static bool PlusFatFile_Exists(struct SolidSyslogFile* base, const char* path) +{ + (void) base; + FF_Stat_t status; + return ff_stat(path, &status) == 0; +} + +static bool PlusFatFile_Delete(struct SolidSyslogFile* base, const char* path) +{ + (void) base; + return ff_remove(path) == 0; +} diff --git a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp index a90bba75..a3ac68cc 100644 --- a/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp +++ b/Tests/PlusFat/SolidSyslogPlusFatFileTest.cpp @@ -191,3 +191,33 @@ TEST(SolidSyslogPlusFatFile, TruncateSeeksToZeroAndCallsFseteof) LONGS_EQUAL(0, PlusFatFake_LastSeekOffset()); CALLED_FAKE(PlusFatFake_Seteof, ONCE); } + +TEST(SolidSyslogPlusFatFile, ExistsCallsFstatAndReportsTrue) +{ + CHECK_TRUE(SolidSyslogFile_Exists(file, TEST_PATH)); + + CALLED_FAKE(PlusFatFake_Stat, ONCE); + STRCMP_EQUAL(TEST_PATH, PlusFatFake_LastStatPath()); +} + +TEST(SolidSyslogPlusFatFile, ExistsReportsFalseWhenFstatFails) +{ + PlusFatFake_SetStatFails(); + + CHECK_FALSE(SolidSyslogFile_Exists(file, TEST_PATH)); +} + +TEST(SolidSyslogPlusFatFile, DeleteCallsFremoveAndReportsTrue) +{ + CHECK_TRUE(SolidSyslogFile_Delete(file, TEST_PATH)); + + CALLED_FAKE(PlusFatFake_Remove, ONCE); + STRCMP_EQUAL(TEST_PATH, PlusFatFake_LastRemovePath()); +} + +TEST(SolidSyslogPlusFatFile, DeleteReportsFalseWhenFremoveFails) +{ + PlusFatFake_SetRemoveFails(); + + CHECK_FALSE(SolidSyslogFile_Delete(file, TEST_PATH)); +} diff --git a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h index e55d7aa7..89a49a98 100644 --- a/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h +++ b/Tests/Support/PlusFatFakes/Interface/PlusFatFake.h @@ -44,6 +44,16 @@ EXTERN_C_BEGIN /* ff_seteof */ int PlusFatFake_SeteofCallCount(void); + /* ff_stat */ + void PlusFatFake_SetStatFails(void); + int PlusFatFake_StatCallCount(void); + const char* PlusFatFake_LastStatPath(void); + + /* ff_remove */ + void PlusFatFake_SetRemoveFails(void); + int PlusFatFake_RemoveCallCount(void); + const char* PlusFatFake_LastRemovePath(void); + EXTERN_C_END #endif /* PLUSFATFAKE_H */ diff --git a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c index e58a0fc0..b50c680c 100644 --- a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c +++ b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c @@ -53,6 +53,16 @@ static size_t fileLength; /* ff_seteof state */ static int seteofCallCount; +/* ff_stat state */ +static int statCallCount; +static const char* lastStatPath; +static int statResult; + +/* ff_remove state */ +static int removeCallCount; +static const char* lastRemovePath; +static int removeResult; + void PlusFatFake_Reset(void) { openCallCount = 0; @@ -77,6 +87,12 @@ void PlusFatFake_Reset(void) lastSeekWhence = 0; fileLength = 0; seteofCallCount = 0; + statCallCount = 0; + lastStatPath = NULL; + statResult = 0; + removeCallCount = 0; + lastRemovePath = NULL; + removeResult = 0; } void PlusFatFake_SetOpenFailsForMode(const char* mode) @@ -268,3 +284,48 @@ int ff_seteof(FF_FILE* pxStream) seteofCallCount++; return 0; } + +void PlusFatFake_SetStatFails(void) +{ + statResult = -1; +} + +int PlusFatFake_StatCallCount(void) +{ + return statCallCount; +} + +const char* PlusFatFake_LastStatPath(void) +{ + return lastStatPath; +} + +int ff_stat(const char* pcFileName, FF_Stat_t* pxStatBuffer) +{ + (void) pxStatBuffer; + statCallCount++; + lastStatPath = pcFileName; + return statResult; +} + +void PlusFatFake_SetRemoveFails(void) +{ + removeResult = -1; +} + +int PlusFatFake_RemoveCallCount(void) +{ + return removeCallCount; +} + +const char* PlusFatFake_LastRemovePath(void) +{ + return lastRemovePath; +} + +int ff_remove(const char* pcPath) +{ + removeCallCount++; + lastRemovePath = pcPath; + return removeResult; +} From 87c1cb42c772d7eaacdacd945b456f9518e3bf4e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 3 Jun 2026 22:07:30 +0100 Subject: [PATCH 8/8] chore: S29.04 PlusFat CI analysis wiring + docs Add Platform/PlusFat to the cppcheck / cppcheck-misra command lists in ci.yml and scripts/misra_renumber.py (kept in lockstep); the freertos tidy/iwyu lanes pick the pack up automatically via the configure. Suppress the one vtable downcast under deviation D.002. Apply clang-format and clang-tidy fixes to the new pack and fake (ff_filelength param name, openModes pointer-array init). Add the SolidSyslogPlusFatFile{,Errors}.h rows to the CLAUDE.md header table and the S29.04 DEVLOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 6 +++ CLAUDE.md | 2 + DEVLOG.md | 46 +++++++++++++++++++ .../Source/SolidSyslogPlusFatFileStatic.c | 5 +- .../Interface/FreeRTOSFATConfig.h | 8 ++-- .../Support/PlusFatFakes/Source/PlusFatFake.c | 9 ++-- misra_suppressions.txt | 1 + scripts/misra_renumber.py | 2 + 8 files changed, 70 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a9ef747..789cc33c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -709,6 +709,7 @@ jobs: -IPlatform/PlusTcp/Interface -IPlatform/LwipRaw/Interface -IPlatform/FatFs/Interface + -IPlatform/PlusFat/Interface --xml --xml-version=2 Core/Source/ Platform/Atomics/Source/ @@ -720,6 +721,7 @@ jobs: Platform/PlusTcp/Source/ Platform/LwipRaw/Source/ Platform/FatFs/Source/ + Platform/PlusFat/Source/ 2> build/cppcheck/cppcheck-report.xml - name: Upload cppcheck report @@ -756,6 +758,7 @@ jobs: -IPlatform/PlusTcp/Interface -IPlatform/LwipRaw/Interface -IPlatform/FatFs/Interface + -IPlatform/PlusFat/Interface Core/Source/ Platform/Atomics/Source/ Platform/Posix/Source/ @@ -766,6 +769,7 @@ jobs: Platform/PlusTcp/Source/ Platform/LwipRaw/Source/ Platform/FatFs/Source/ + Platform/PlusFat/Source/ - name: Generate cppcheck-misra XML report if: success() || failure() @@ -787,6 +791,7 @@ jobs: -IPlatform/PlusTcp/Interface \ -IPlatform/LwipRaw/Interface \ -IPlatform/FatFs/Interface \ + -IPlatform/PlusFat/Interface \ --xml --xml-version=2 \ Core/Source/ \ Platform/Atomics/Source/ \ @@ -798,6 +803,7 @@ jobs: Platform/PlusTcp/Source/ \ Platform/LwipRaw/Source/ \ Platform/FatFs/Source/ \ + Platform/PlusFat/Source/ \ 2> build/cppcheck-misra/cppcheck-misra-report.xml - name: Upload cppcheck-misra report diff --git a/CLAUDE.md b/CLAUDE.md index a705a30e..e7f32a77 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -421,6 +421,8 @@ live under `Core/Interface/`; platform-specific helpers (the `SolidSyslogPosix*` | `SolidSyslogWindowsFileErrors.h` | Any code installing an error handler that wants to react to WindowsFile-specific events (pointer-identity match on `WindowsFileErrorSource`, switch on `enum SolidSyslogWindowsFileErrors`) | `enum SolidSyslogWindowsFileErrors` (`WINDOWSFILE_ERROR_*` codes + `WINDOWSFILE_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource WindowsFileErrorSource`. Integrators ignore if not handling errors per source. | | `SolidSyslogFatFsFile.h` | System setup code on targets using ChaN FatFs as the file layer (bare-metal flash / SD / eMMC, FreeRTOS, Zephyr, NuttX, …) | `SolidSyslogFatFsFile_Create(void)`, `_Destroy(file)` (wraps `f_open` / `f_close` / `f_read` / `f_write` with `f_sync` after every successful write so a power loss never loses a record the BlockStore claimed it stored; integrator supplies `diskio.c` and — if `FF_FS_REENTRANT=1` — `ffsystem.c`). Instance struct lives in a library-internal static pool (E11). Pool-exhaustion fallback is the shared `SolidSyslogNullFile`. | | `SolidSyslogFatFsFileErrors.h` | Any code installing an error handler that wants to react to FatFsFile-specific events (pointer-identity match on `FatFsFileErrorSource`, switch on `enum SolidSyslogFatFsFileErrors`) | `enum SolidSyslogFatFsFileErrors` (`FATFSFILE_ERROR_*` codes + `FATFSFILE_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource FatFsFileErrorSource`. Integrators ignore if not handling errors per source. | +| `SolidSyslogPlusFatFile.h` | System setup code on FreeRTOS targets using FreeRTOS-Plus-FAT as the file layer | `SolidSyslogPlusFatFile_Create(void)`, `_Destroy(file)` (wraps `ff_fopen` (`"r+"` then `"w+"` fallback — open-or-create without truncation) / `ff_fclose` / `ff_fread` / `ff_fwrite` with `ff_fflush` after every complete write so a power loss never loses a record the BlockStore claimed it stored; `ff_fseek` / `ff_seteof` for truncate-to-empty; `ff_filelength`, `ff_stat`, `ff_remove`). The open-state is the `FF_FILE*` sentinel (no separate flag). Plus-FAT is FreeRTOS-coupled (`ff_headers.h` pulls `FreeRTOS.h` / `task.h` / `semphr.h`); integrator supplies the `FF_Disk_t` media driver and `FreeRTOSFATConfig.h`. Instance struct lives in a library-internal static pool (E11; role tunable `SOLIDSYSLOG_FILE_POOL_SIZE`). Pool-exhaustion fallback is the shared `SolidSyslogNullFile`. | +| `SolidSyslogPlusFatFileErrors.h` | Any code installing an error handler that wants to react to PlusFatFile-specific events (pointer-identity match on `PlusFatFileErrorSource`, switch on `enum SolidSyslogPlusFatFileErrors`) | `enum SolidSyslogPlusFatFileErrors` (`PLUSFATFILE_ERROR_*` codes + `PLUSFATFILE_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource PlusFatFileErrorSource`. Integrators ignore if not handling errors per source. | | `SolidSyslogPosixClock.h` | System setup code using POSIX clock | `SolidSyslogPosixClock_GetTimestamp` | | `SolidSyslogPosixHostname.h` | String callback implementor using POSIX hostname | `SolidSyslogPosixHostname_Get` (writes into `SolidSyslogFormatter*`) | | `SolidSyslogPosixProcessId.h` | String callback implementor using POSIX process ID | `SolidSyslogPosixProcessId_Get` (writes into `SolidSyslogFormatter*`) | diff --git a/DEVLOG.md b/DEVLOG.md index ed821406..d68c10bd 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,51 @@ # Dev Log +## 2026-06-03 — S29.04 Platform/PlusFat adapter pack (host TDD) + +The FreeRTOS-Plus-FAT `SolidSyslogFile` adapter, mirroring the FatFs sibling but +adapted to the `ff_stdio` API. No media driver, no QEMU, no target swap — that is +S29.05; this story is the host-tested pack only. + +### Harness spike first + +Plus-FAT is FreeRTOS-coupled (unlike OS-agnostic ChaN FatFs): `ff_stdio.h` → +`ff_headers.h` pulls `FreeRTOS.h` / `task.h` / `semphr.h`. A throwaway spike proved +a host TU compiles `ff_stdio.h` given (1) a test-local `FreeRTOSFATConfig.h` +(byte-order, CWD TLS index, `portINLINE` fallback — matching Plus-FAT's own +DefaultConf), (2) `configNUM_THREAD_LOCAL_STORAGE_POINTERS >= 3` in the shared +`FreeRtosFakes/FreeRTOSConfig.h` (Plus-FAT keeps errno/CWD/ff_error in TLS slots), +and (3) the kernel + Plus-FAT include dirs. We surface that coupling in the test +include path rather than hiding it. + +### Pool experience copied, behaviour TDD'd + +The pool plumbing (`Static.c`) and its 9-test suite are **copied + renamed +verbatim** from FatFs — proven, agreed coverage, not re-derived. Everything +`ff_*` was driven by genuine red→green: Open (`"r+"`→`"w+"` fallback, +open-or-create without truncation), Close (guarded `ff_fclose`), Read +(`ff_fread`, item-count == requested), Write + per-write `ff_fflush` durability, +SeekTo/Size, Truncate (`ff_fseek(0)`+`ff_seteof`), Exists/Delete. 30 tests, 100% +line+branch by construction. + +### Decisions + +- **`FF_FILE*` sentinel is the open-state** — no separate `IsOpen` bool (FatFs + needs one because its `FIL` is embedded; Plus-FAT's handle is a pointer). +- **Adapter calls `ff_fread`/`ff_fwrite` with `xSize == 1`** so the returned item + count is the byte count; a short read/write is the single failure mode. +- **D.002 11.3 suppression** added for the one `SelfFromBase` downcast, mirroring + every other vtable class. + +### Lesson — TDD, not TDD-costume + +First cut wrote the whole pool allocator to satisfy a trivial `CreateSucceeds`, +then dressed it as a slice. Reverted to the literal minimum; the rule for these +pool classes is copy+rename the proven pool block (plumbing **and** its tests) +for instant agreed coverage, then do real per-op TDD for the rest. Also caught two +invalid reds that passed for free through the `NullFile` fallback +(`IsOpen`-false, `Write`-returns-true) — the genuine driver is asserting the call +reached the fake. + ## 2026-06-03 — S29.03 Extract the shared FreeRTOS BDD-target pipeline The DRY pass S29.01 deferred. The two FreeRTOS target `main.c` files diff --git a/Platform/PlusFat/Source/SolidSyslogPlusFatFileStatic.c b/Platform/PlusFat/Source/SolidSyslogPlusFatFileStatic.c index 4097675c..a05e95c9 100644 --- a/Platform/PlusFat/Source/SolidSyslogPlusFatFileStatic.c +++ b/Platform/PlusFat/Source/SolidSyslogPlusFatFileStatic.c @@ -45,8 +45,9 @@ struct SolidSyslogFile* SolidSyslogPlusFatFile_Create(void) void SolidSyslogPlusFatFile_Destroy(struct SolidSyslogFile* base) { size_t index = PlusFatFile_IndexFromHandle(base); - bool released = SolidSyslogPoolAllocator_IndexIsValid(&PlusFatFile_Allocator, index) && - SolidSyslogPoolAllocator_FreeIfInUse(&PlusFatFile_Allocator, index, PlusFatFile_CleanupAtIndex, NULL); + bool released = + SolidSyslogPoolAllocator_IndexIsValid(&PlusFatFile_Allocator, index) && + SolidSyslogPoolAllocator_FreeIfInUse(&PlusFatFile_Allocator, index, PlusFatFile_CleanupAtIndex, NULL); if (!released) { PlusFatFile_Report( diff --git a/Tests/Support/PlusFatFakes/Interface/FreeRTOSFATConfig.h b/Tests/Support/PlusFatFakes/Interface/FreeRTOSFATConfig.h index 04582c08..8d35936a 100644 --- a/Tests/Support/PlusFatFakes/Interface/FreeRTOSFATConfig.h +++ b/Tests/Support/PlusFatFakes/Interface/FreeRTOSFATConfig.h @@ -12,11 +12,11 @@ #ifndef FREERTOSFATCONFIG_H #define FREERTOSFATCONFIG_H -#define ffconfigBYTE_ORDER ( pdFREERTOS_LITTLE_ENDIAN ) -#define ffconfigCWD_THREAD_LOCAL_INDEX ( 0 ) +#define ffconfigBYTE_ORDER (pdFREERTOS_LITTLE_ENDIAN) +#define ffconfigCWD_THREAD_LOCAL_INDEX (0) -#if !defined( portINLINE ) - #define portINLINE __inline +#if !defined(portINLINE) +#define portINLINE __inline #endif #endif /* FREERTOSFATCONFIG_H */ diff --git a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c index b50c680c..64e6854f 100644 --- a/Tests/Support/PlusFatFakes/Source/PlusFatFake.c +++ b/Tests/Support/PlusFatFakes/Source/PlusFatFake.c @@ -69,7 +69,10 @@ void PlusFatFake_Reset(void) lastOpenPath = NULL; openFailMode = NULL; openAlwaysFails = false; - memset(openModes, 0, sizeof(openModes)); + for (size_t modeIndex = 0; modeIndex < OPEN_MODE_CAPACITY; modeIndex++) + { + openModes[modeIndex] = NULL; + } closeCallCount = 0; memset(readSource, 0, sizeof(readSource)); readSourceCount = 0; @@ -267,9 +270,9 @@ void PlusFatFake_SetFileLength(unsigned long length) fileLength = (size_t) length; } -size_t ff_filelength(FF_FILE* pxStream) +size_t ff_filelength(FF_FILE* pxFile) { - (void) pxStream; + (void) pxFile; return fileLength; } diff --git a/misra_suppressions.txt b/misra_suppressions.txt index b12ce644..8a00fcd3 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -37,6 +37,7 @@ misra-c2012-11.3:Core/Source/SolidSyslogTimeQualitySd.c:63 misra-c2012-11.3:Core/Source/SolidSyslogUdpSender.c:100 misra-c2012-11.3:Platform/Atomics/Source/SolidSyslogStdAtomicCounter.c:29 misra-c2012-11.3:Platform/FatFs/Source/SolidSyslogFatFsFile.c:49 +misra-c2012-11.3:Platform/PlusFat/Source/SolidSyslogPlusFatFile.c:45 misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawAddressPrivate.h:29 misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawAddressPrivate.h:36 misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawAddressStatic.c:35 diff --git a/scripts/misra_renumber.py b/scripts/misra_renumber.py index ca3bb23b..c040259b 100755 --- a/scripts/misra_renumber.py +++ b/scripts/misra_renumber.py @@ -78,6 +78,7 @@ "-IPlatform/PlusTcp/Interface", "-IPlatform/LwipRaw/Interface", "-IPlatform/FatFs/Interface", + "-IPlatform/PlusFat/Interface", "--xml", "--xml-version=2", "Core/Source/", @@ -90,6 +91,7 @@ "Platform/PlusTcp/Source/", "Platform/LwipRaw/Source/", "Platform/FatFs/Source/", + "Platform/PlusFat/Source/", ] SUPPRESSION_LINE = re.compile(r"^(misra-c2012-[0-9.]+):([^:]+):(\d+)\s*$")