From 73fdd28f745bc7875cd0284984caeb1988f2c722 Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Thu, 16 Apr 2026 13:48:25 +1000 Subject: [PATCH] Harden DFTE rendering and placeholder registration to fail safely under stalled and invalid inputs. Add regression coverage and docs updates so these edge cases stay stable across board test runs. --- README.md | 26 ++--- include/DeviceFrameworkPlaceholderRegistry.h | 2 +- include/DeviceFrameworkTemplateTypes.h | 6 +- include/TemplateEngine.h | 7 -- src/DeviceFrameworkPlaceholderRegistry.cpp | 94 +++++++++++++++++-- src/DeviceFrameworkTemplateContext.cpp | 10 +- src/DeviceFrameworkTemplateRenderer.cpp | 45 ++++++++- test/test_template_engine/test_main.cpp | 2 + test/test_template_engine/test_main.h | 2 + .../tests/test_edge_cases.cpp | 14 ++- .../tests/test_placeholder_registry.cpp | 5 + .../tests/test_template_renderer.cpp | 87 +++++++++++++++++ 12 files changed, 262 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index f1b8b01..5e02ace 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,11 @@ DFTE is a lightweight C++ template engine tailored for Arduino-class hardware (E Serial.begin(115200); registry.registerProgmemData(PSTR("%APP_TITLE%"), PSTR("DFTE Quickstart")); - registry.registerRamData(PSTR("%UPTIME%"), [](PlaceholderWriter& w) { - static char buffer[16]; - snprintf(buffer, sizeof(buffer), "%lus", millis() / 1000); - w.write(buffer); - }); + registry.registerRamData(PSTR("%UPTIME%"), []() -> const char* { + static char buffer[16]; + snprintf(buffer, sizeof(buffer), "%lus", millis() / 1000); + return buffer; + }); registry.registerProgmemTemplate(PSTR("%ROOT%"), ROOT_TEMPLATE_PROGMEM); ctx.setRegistry(®istry); @@ -100,11 +100,11 @@ All public headers are re-exported from `TemplateEngine.h`, so typical sketches ``` PlaceholderRegistry registry; registry.registerProgmemData(PSTR("%APP_TITLE%"), PSTR("DFTE Dashboard")); - registry.registerRamData(PSTR("%UPTIME%"), [](PlaceholderWriter& w) { - static char buffer[16]; - snprintf(buffer, sizeof(buffer), "%lus", millis() / 1000); - w.write(buffer); - }); + registry.registerRamData(PSTR("%UPTIME%"), []() -> const char* { + static char buffer[16]; + snprintf(buffer, sizeof(buffer), "%lus", millis() / 1000); + return buffer; + }); registry.registerProgmemTemplate(PSTR("%ROOT%"), ROOT_TEMPLATE_PROGMEM); ``` @@ -149,10 +149,10 @@ std::shared_ptr registry; void setupRegistry() { registry = std::make_shared(); registry->registerProgmemData(PSTR("%APP_TITLE%"), PSTR("DFTE Async Portal")); - registry->registerRamData(PSTR("%UPTIME%"), [](PlaceholderWriter& w) { + registry->registerRamData(PSTR("%UPTIME%"), []() -> const char* { static char buffer[16]; snprintf(buffer, sizeof(buffer), "%lus", millis() / 1000); - w.write(buffer); + return buffer; }); registry->registerProgmemTemplate(PSTR("%ROOT%"), ROOT_TEMPLATE_PROGMEM); } @@ -206,7 +206,7 @@ Every placeholder in a template uses `%NAME%`. DFTE looks up `NAME` in the regis - **Static data** – `registerProgmemData("%CSS%", PROGMEM_BLOCK)` streams literal content from flash or RAM. - **Nested template** – `registerProgmemTemplate("%HEADER%", HEADER_TEMPLATE)` injects another template that can contain its own placeholders. -- **Dynamic value** – `registerRamData("%UPTIME%", getter)` calls a function that writes the current value into the output buffer. +- **Dynamic value** – `registerRamData("%UPTIME%", getter)` calls a function that returns the current value as a `const char*`. - **Dynamic template** – `registerDynamicTemplate("%CONTENT%", &DynamicTemplateDescriptor{getter, getLength, userData})` asks your getter to return template text at render time. - **Conditional** – `registerConditional("%IS_ONLINE%", &ConditionalDescriptor{evaluate, "%ONLINE%", "%OFFLINE%", userData})` chooses which delegate placeholder to render based on the evaluator result. - **Iterator** – `registerIterator("%SENSORS%", &IteratorDescriptor{open, next, close, userData})` opens a handle, streams each item template through `IteratorItemView`, and finalises with `close`. diff --git a/include/DeviceFrameworkPlaceholderRegistry.h b/include/DeviceFrameworkPlaceholderRegistry.h index 17a8e3b..400cea3 100644 --- a/include/DeviceFrameworkPlaceholderRegistry.h +++ b/include/DeviceFrameworkPlaceholderRegistry.h @@ -127,7 +127,7 @@ public: static size_t getDynamicTemplateLength(const DynamicTemplateDescriptor* descriptor, const char* templateData); private: - static constexpr uint16_t MAX_PLACEHOLDER_NAME_SIZE = DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT; + static constexpr uint16_t MAX_PLACEHOLDER_NAME_SIZE = DFTE_PLACEHOLDER_NAME_SIZE; PlaceholderEntry* placeholders; // Dynamically allocated array uint16_t maxPlaceholders; // Configurable size diff --git a/include/DeviceFrameworkTemplateTypes.h b/include/DeviceFrameworkTemplateTypes.h index 0ae49cc..aa92426 100644 --- a/include/DeviceFrameworkTemplateTypes.h +++ b/include/DeviceFrameworkTemplateTypes.h @@ -87,11 +87,15 @@ struct PlaceholderEntry { PlaceholderType type; const void* data; PlaceholderLengthGetter getLength; + size_t cachedLength; + bool hasCachedLength; PlaceholderEntry() : type(PlaceholderType::RAM_DATA), data(nullptr), - getLength(nullptr) { + getLength(nullptr), + cachedLength(0), + hasCachedLength(false) { name[0] = '\0'; } }; diff --git a/include/TemplateEngine.h b/include/TemplateEngine.h index 8164bf3..c4f9572 100644 --- a/include/TemplateEngine.h +++ b/include/TemplateEngine.h @@ -44,12 +44,5 @@ using TemplateRenderer = DeviceFrameworkTemplateRenderer; using TemplateContext = DeviceFrameworkTemplateContext; using PlaceholderRegistry = DeviceFrameworkPlaceholderRegistry; -// Use new type names -using PlaceholderType = PlaceholderType; -using PlaceholderEntry = PlaceholderEntry; -using RenderingContextType = RenderingContextType; -using RenderingContext = RenderingContext; -using TemplateRenderState = TemplateRenderState; - #endif // TEMPLATE_ENGINE_H diff --git a/src/DeviceFrameworkPlaceholderRegistry.cpp b/src/DeviceFrameworkPlaceholderRegistry.cpp index 20955ae..f6112ca 100644 --- a/src/DeviceFrameworkPlaceholderRegistry.cpp +++ b/src/DeviceFrameworkPlaceholderRegistry.cpp @@ -1,11 +1,26 @@ #include "DeviceFrameworkPlaceholderRegistry.h" #include "DeviceFrameworkTemplateEngineDebug.h" #include +#include DeviceFrameworkPlaceholderRegistry::DeviceFrameworkPlaceholderRegistry(uint16_t maxPlaceholders) - : maxPlaceholders(maxPlaceholders), count(0) { - placeholders = new PlaceholderEntry[maxPlaceholders]; - memset(placeholders, 0, sizeof(PlaceholderEntry) * maxPlaceholders); + : placeholders(nullptr), maxPlaceholders(maxPlaceholders), count(0) { + if (maxPlaceholders == 0) { + DFTE_LOG_ERROR("Placeholder registry size cannot be zero"); + this->maxPlaceholders = 0; + return; + } + + placeholders = new (std::nothrow) PlaceholderEntry[maxPlaceholders]; + if (placeholders == nullptr) { + DFTE_LOG_ERROR("Failed to allocate placeholder registry"); + this->maxPlaceholders = 0; + return; + } + + for (uint16_t i = 0; i < maxPlaceholders; ++i) { + placeholders[i] = PlaceholderEntry(); + } } DeviceFrameworkPlaceholderRegistry::~DeviceFrameworkPlaceholderRegistry() { @@ -16,6 +31,11 @@ DeviceFrameworkPlaceholderRegistry::~DeviceFrameworkPlaceholderRegistry() { } bool DeviceFrameworkPlaceholderRegistry::registerProgmemData(const char* name, const char* progmemData) { + if (placeholders == nullptr || maxPlaceholders == 0) { + DFTE_LOG_ERROR("Placeholder registry not initialized"); + return false; + } + if (count >= maxPlaceholders) { DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); return false; @@ -37,12 +57,19 @@ bool DeviceFrameworkPlaceholderRegistry::registerProgmemData(const char* name, c entry.type = PlaceholderType::PROGMEM_DATA; entry.data = progmemData; entry.getLength = getProgmemLength; + entry.cachedLength = getProgmemLength(progmemData); + entry.hasCachedLength = true; count++; return true; } bool DeviceFrameworkPlaceholderRegistry::registerProgmemTemplate(const char* name, const char* progmemTemplate) { + if (placeholders == nullptr || maxPlaceholders == 0) { + DFTE_LOG_ERROR("Placeholder registry not initialized"); + return false; + } + if (count >= maxPlaceholders) { DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); return false; @@ -58,12 +85,19 @@ bool DeviceFrameworkPlaceholderRegistry::registerProgmemTemplate(const char* nam entry.type = PlaceholderType::PROGMEM_TEMPLATE; entry.data = progmemTemplate; entry.getLength = getProgmemLength; + entry.cachedLength = getProgmemLength(progmemTemplate); + entry.hasCachedLength = true; count++; return true; } bool DeviceFrameworkPlaceholderRegistry::registerRamData(const char* name, PlaceholderDataGetter getter) { + if (placeholders == nullptr || maxPlaceholders == 0) { + DFTE_LOG_ERROR("Placeholder registry not initialized"); + return false; + } + if (count >= maxPlaceholders) { DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); return false; @@ -84,12 +118,19 @@ bool DeviceFrameworkPlaceholderRegistry::registerRamData(const char* name, Place entry.type = PlaceholderType::RAM_DATA; entry.data = (const void*)getter; entry.getLength = getRamLength; + entry.cachedLength = 0; + entry.hasCachedLength = false; count++; return true; } bool DeviceFrameworkPlaceholderRegistry::registerDynamicTemplate(const char* name, const DynamicTemplateDescriptor* descriptor) { + if (placeholders == nullptr || maxPlaceholders == 0) { + DFTE_LOG_ERROR("Placeholder registry not initialized"); + return false; + } + if (count >= maxPlaceholders) { DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); return false; @@ -110,12 +151,19 @@ bool DeviceFrameworkPlaceholderRegistry::registerDynamicTemplate(const char* nam entry.type = PlaceholderType::DYNAMIC_TEMPLATE; entry.data = descriptor; entry.getLength = nullptr; + entry.cachedLength = 0; + entry.hasCachedLength = false; count++; return true; } bool DeviceFrameworkPlaceholderRegistry::registerConditional(const char* name, const ConditionalDescriptor* descriptor) { + if (placeholders == nullptr || maxPlaceholders == 0) { + DFTE_LOG_ERROR("Placeholder registry not initialized"); + return false; + } + if (count >= maxPlaceholders) { DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); return false; @@ -136,12 +184,19 @@ bool DeviceFrameworkPlaceholderRegistry::registerConditional(const char* name, c entry.type = PlaceholderType::CONDITIONAL; entry.data = descriptor; entry.getLength = nullptr; + entry.cachedLength = 0; + entry.hasCachedLength = false; count++; return true; } bool DeviceFrameworkPlaceholderRegistry::registerIterator(const char* name, const IteratorDescriptor* descriptor) { + if (placeholders == nullptr || maxPlaceholders == 0) { + DFTE_LOG_ERROR("Placeholder registry not initialized"); + return false; + } + if (count >= maxPlaceholders) { DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); return false; @@ -162,6 +217,8 @@ bool DeviceFrameworkPlaceholderRegistry::registerIterator(const char* name, cons entry.type = PlaceholderType::ITERATOR; entry.data = descriptor; entry.getLength = nullptr; + entry.cachedLength = 0; + entry.hasCachedLength = false; count++; return true; @@ -169,7 +226,13 @@ bool DeviceFrameworkPlaceholderRegistry::registerIterator(const char* name, cons void DeviceFrameworkPlaceholderRegistry::clear() { count = 0; - memset(placeholders, 0, sizeof(PlaceholderEntry) * maxPlaceholders); + if (placeholders == nullptr || maxPlaceholders == 0) { + return; + } + + for (uint16_t i = 0; i < maxPlaceholders; ++i) { + placeholders[i] = PlaceholderEntry(); + } } bool DeviceFrameworkPlaceholderRegistry::validatePlaceholderName(const char* name) const { @@ -194,7 +257,7 @@ bool DeviceFrameworkPlaceholderRegistry::validatePlaceholderName(const char* nam } const PlaceholderEntry* DeviceFrameworkPlaceholderRegistry::getPlaceholder(const char* name) const { - if (name == nullptr) return nullptr; + if (name == nullptr || placeholders == nullptr || count <= 0) return nullptr; for (int i = count - 1; i >= 0; i--) { if (strcmp(placeholders[i].name, name) == 0) { @@ -206,14 +269,29 @@ const PlaceholderEntry* DeviceFrameworkPlaceholderRegistry::getPlaceholder(const size_t DeviceFrameworkPlaceholderRegistry::renderPlaceholder(const PlaceholderEntry* entry, size_t offset, uint8_t* buffer, size_t maxLen) const { - if (entry == nullptr || maxLen == 0) { + if (entry == nullptr || buffer == nullptr || maxLen == 0) { return 0; } switch (entry->type) { case PlaceholderType::PROGMEM_DATA: - case PlaceholderType::PROGMEM_TEMPLATE: - return copyProgmemData((const char*)entry->data, offset, buffer, maxLen); + case PlaceholderType::PROGMEM_TEMPLATE: { + const char* data = static_cast(entry->data); + if (data == nullptr) { + return 0; + } + + size_t dataLen = entry->hasCachedLength ? entry->cachedLength : getProgmemLength(entry->data); + if (offset >= dataLen) { + return 0; + } + + size_t remaining = dataLen - offset; + constexpr size_t MAX_CHUNK = DFTE_PROGMEM_CHUNK_SIZE; + size_t chunkSize = min(min(maxLen, remaining), MAX_CHUNK); + memcpy_P(buffer, data + offset, chunkSize); + return chunkSize; + } case PlaceholderType::RAM_DATA: return copyRamData((PlaceholderDataGetter)entry->data, offset, buffer, maxLen); diff --git a/src/DeviceFrameworkTemplateContext.cpp b/src/DeviceFrameworkTemplateContext.cpp index 47f18f4..3fe4ffc 100644 --- a/src/DeviceFrameworkTemplateContext.cpp +++ b/src/DeviceFrameworkTemplateContext.cpp @@ -7,7 +7,9 @@ DeviceFrameworkTemplateContext::DeviceFrameworkTemplateContext() registry(nullptr), totalBytesProcessed(0), startTime(0) { memset(placeholderName, 0, sizeof(placeholderName)); - memset(renderingStack, 0, sizeof(renderingStack)); + for (int i = 0; i < MAX_RENDERING_DEPTH; ++i) { + renderingStack[i] = RenderingContext(); + } } void DeviceFrameworkTemplateContext::reset() { @@ -20,7 +22,9 @@ void DeviceFrameworkTemplateContext::reset() { totalBytesProcessed = 0; startTime = millis(); memset(placeholderName, 0, sizeof(placeholderName)); - memset(renderingStack, 0, sizeof(renderingStack)); + for (int i = 0; i < MAX_RENDERING_DEPTH; ++i) { + renderingStack[i] = RenderingContext(); + } } // Unified stack management methods @@ -107,7 +111,7 @@ void DeviceFrameworkTemplateContext::popContext() { } // Clear the popped context - memset(&ctx, 0, sizeof(RenderingContext)); + ctx = RenderingContext(); } RenderingContext* DeviceFrameworkTemplateContext::getCurrentContext() { diff --git a/src/DeviceFrameworkTemplateRenderer.cpp b/src/DeviceFrameworkTemplateRenderer.cpp index 0af4853..24cee77 100644 --- a/src/DeviceFrameworkTemplateRenderer.cpp +++ b/src/DeviceFrameworkTemplateRenderer.cpp @@ -704,8 +704,21 @@ DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer:: case RenderingContextType::PLACEHOLDER_DYNAMIC_TEMPLATE: return makeState(TemplateRenderState::TEXT, true); - case RenderingContextType::PLACEHOLDER_CONDITIONAL: - return makeState(TemplateRenderState::RENDERING_CONTEXT, true); + case RenderingContextType::PLACEHOLDER_CONDITIONAL: { + RenderOutcome outcome = makeState(TemplateRenderState::RENDERING_CONTEXT, true); + outcome.popCount = 1; + + RenderingContext* parent = (ctx.renderingDepth > 1) ? ctx.getContext(ctx.renderingDepth - 2) : nullptr; + if (!parent) { + outcome.nextState = TemplateRenderState::COMPLETE; + outcome.repeat = false; + outcome.finished = true; + } else if (parent->type == RenderingContextType::TEMPLATE) { + outcome.nextState = TemplateRenderState::TEXT; + } + + return outcome; + } case RenderingContextType::PLACEHOLDER_ITERATOR: return processIteratorContext(ctx, currentCtx); @@ -747,7 +760,16 @@ DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer:: return outcome; } - size_t totalLength = entry->getLength(entry->data); + size_t totalLength = 0; + if (entry->hasCachedLength) { + totalLength = entry->cachedLength; + } else if (entry->getLength != nullptr) { + totalLength = entry->getLength(entry->data); + } else { + DFTE_LOG_ERROR("Placeholder '" + String(entry->name) + "' missing length getter"); + return makeError(); + } + if (dataCtx.offset >= totalLength) { RenderOutcome outcome = makeState(TemplateRenderState::RENDERING_CONTEXT, true); outcome.popCount = 1; @@ -799,12 +821,20 @@ size_t DeviceFrameworkTemplateRenderer::renderNextChunk(DeviceFrameworkTemplateC size_t written = 0; size_t iterations = 0; + size_t consecutiveNoProgressIterations = 0; uint8_t* writePtr = buffer; size_t remaining = maxLen; while (remaining > 0 && !ctx.isComplete() && !ctx.hasError() && iterations < MAX_ITERATIONS) { RenderOutcome outcome = renderChunk(ctx, writePtr, remaining); + bool noProgressIteration = (outcome.bytesWritten == 0 && outcome.repeat && !outcome.finished && !outcome.errored); + if (noProgressIteration) { + consecutiveNoProgressIterations++; + } else { + consecutiveNoProgressIterations = 0; + } + written += outcome.bytesWritten; ctx.totalBytesProcessed += outcome.bytesWritten; writePtr += outcome.bytesWritten; @@ -822,6 +852,9 @@ size_t DeviceFrameworkTemplateRenderer::renderNextChunk(DeviceFrameworkTemplateC if (iterations >= MAX_ITERATIONS) { DFTE_LOG_WARN("Maximum iterations reached in renderNextChunk"); + if (consecutiveNoProgressIterations >= 3 && !ctx.isComplete() && !ctx.hasError()) { + ctx.state = TemplateRenderState::ERROR; + } } return written; @@ -833,6 +866,12 @@ void DeviceFrameworkTemplateRenderer::initializeContext(DeviceFrameworkTemplateC void DeviceFrameworkTemplateRenderer::initializeContext(DeviceFrameworkTemplateContext& ctx, const char* templateData, bool templateInProgmem) { ctx.reset(); + + if (templateData == nullptr) { + DFTE_LOG_ERROR("initializeContext called with null template pointer"); + ctx.state = TemplateRenderState::ERROR; + return; + } // Push initial template context if (!ctx.pushContext(RenderingContextType::TEMPLATE, "ROOT")) { diff --git a/test/test_template_engine/test_main.cpp b/test/test_template_engine/test_main.cpp index 9c81ff6..5bec169 100644 --- a/test/test_template_engine/test_main.cpp +++ b/test/test_template_engine/test_main.cpp @@ -68,10 +68,12 @@ TestCase tests[] = { TEST_ENTRY(test_template_renderer_conditional_skip), TEST_ENTRY(test_template_renderer_conditional_missing_delegate), TEST_ENTRY(test_template_renderer_conditional_nested_iterator), + TEST_ENTRY(test_template_renderer_conditional_template_delegate_completes), TEST_ENTRY(test_template_renderer_iterator_basic), TEST_ENTRY(test_template_renderer_iterator_empty), TEST_ENTRY(test_template_renderer_iterator_dynamic_items), TEST_ENTRY(test_template_renderer_iterator_error_cleanup), + TEST_ENTRY(test_template_renderer_iterator_stall_guard), // Group 4: Integration Tests TEST_ENTRY(test_integration_full_rendering), diff --git a/test/test_template_engine/test_main.h b/test/test_template_engine/test_main.h index 2a6eb2c..b591f63 100644 --- a/test/test_template_engine/test_main.h +++ b/test/test_template_engine/test_main.h @@ -39,10 +39,12 @@ void test_template_renderer_conditional_false_branch(); void test_template_renderer_conditional_skip(); void test_template_renderer_conditional_missing_delegate(); void test_template_renderer_conditional_nested_iterator(); +void test_template_renderer_conditional_template_delegate_completes(); void test_template_renderer_iterator_basic(); void test_template_renderer_iterator_empty(); void test_template_renderer_iterator_dynamic_items(); void test_template_renderer_iterator_error_cleanup(); +void test_template_renderer_iterator_stall_guard(); // Group 4: Integration Tests void test_integration_full_rendering(); diff --git a/test/test_template_engine/tests/test_edge_cases.cpp b/test/test_template_engine/tests/test_edge_cases.cpp index cd4cf3d..0eae9b5 100644 --- a/test/test_template_engine/tests/test_edge_cases.cpp +++ b/test/test_template_engine/tests/test_edge_cases.cpp @@ -26,8 +26,18 @@ void test_edge_cases_error_handling() { TemplateContext ctx; ctx.setRegistry(®istry); - // Test render with null template pointer (should handle gracefully) - // Note: initializeContext expects valid PROGMEM pointer, so we'll test with empty template + // Test render with null template pointer (should enter error cleanly) + TemplateRenderer::initializeContext(ctx, nullptr, false); + TEST_ASSERT_TRUE_MESSAGE(ctx.hasError(), + "Null template pointer should set context to error"); + uint8_t nullBuffer[8]; + size_t nullWritten = TemplateRenderer::renderNextChunk(ctx, nullBuffer, sizeof(nullBuffer)); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0u, static_cast(nullWritten), + "Null template render should produce no output"); + + // Test render with empty template + ctx.reset(); + ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, empty_template); String result1 = captureRenderedOutput(ctx); TEST_ASSERT_EQUAL_STRING_MESSAGE("", result1.c_str(), diff --git a/test/test_template_engine/tests/test_placeholder_registry.cpp b/test/test_template_engine/tests/test_placeholder_registry.cpp index d91dc50..a8c5cbc 100644 --- a/test/test_template_engine/tests/test_placeholder_registry.cpp +++ b/test/test_template_engine/tests/test_placeholder_registry.cpp @@ -133,6 +133,7 @@ void test_placeholder_registry_rendering() { registry.registerProgmemData("%CSS%", test_css_data); const PlaceholderEntry* entry1 = registry.getPlaceholder("%CSS%"); TEST_ASSERT_NOT_NULL_MESSAGE(entry1, "Should find PROGMEM_DATA placeholder"); + TEST_ASSERT_TRUE_MESSAGE(entry1->hasCachedLength, "PROGMEM placeholder should cache length"); // Test render PROGMEM_DATA (full) uint8_t buffer1[256]; @@ -145,6 +146,9 @@ void test_placeholder_registry_rendering() { // Compare with PROGMEM data using strlen_P size_t expectedLen = strlen_P(test_css_data); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(static_cast(expectedLen), + static_cast(entry1->cachedLength), + "Cached PROGMEM length should match source length"); TEST_ASSERT_EQUAL_MESSAGE(expectedLen, len1, "Should render full PROGMEM_DATA length"); // Compare first few characters to verify content @@ -169,6 +173,7 @@ void test_placeholder_registry_rendering() { registry.registerRamData("%TITLE%", getTestRamData); const PlaceholderEntry* entry2 = registry.getPlaceholder("%TITLE%"); TEST_ASSERT_NOT_NULL_MESSAGE(entry2, "Should find RAM_DATA placeholder"); + TEST_ASSERT_FALSE_MESSAGE(entry2->hasCachedLength, "RAM placeholder should not cache length"); // Test render RAM_DATA (full) uint8_t buffer3[256]; diff --git a/test/test_template_engine/tests/test_template_renderer.cpp b/test/test_template_engine/tests/test_template_renderer.cpp index af96c7e..e26894d 100644 --- a/test/test_template_engine/tests/test_template_renderer.cpp +++ b/test/test_template_engine/tests/test_template_renderer.cpp @@ -273,6 +273,13 @@ static ConditionalDescriptor iteratorConditionalDescriptor = { &conditionalState }; +static ConditionalDescriptor conditionalTemplateDescriptor = { + evaluateConditionalState, + "%COND_TMPL_TRUE%", + "%COND_TMPL_FALSE%", + &conditionalState +}; + struct WifiIteratorState { size_t index; size_t count; @@ -408,6 +415,49 @@ static const char PROGMEM conditionalWrapperTemplate[] = "Status:%STATUS_BLOCK%: static const char PROGMEM conditionalSkipTemplate[] = "Value[%STATUS_BLOCK%]"; static const char PROGMEM iteratorWrapperTemplate[] = "
    %WIFI_LIST%
"; static const char PROGMEM iteratorConditionalWrapperTemplate[] = "Badge:
    %WIFI_BADGE%
"; +static const char PROGMEM conditionalTemplateWrapperTemplate[] = "Cond:%COND_TMPL_BLOCK%:Done"; +static const char PROGMEM conditionalTemplateTrueTemplate[] = "Delegate True"; +static const char PROGMEM conditionalTemplateFalseTemplate[] = "Delegate False"; +static const char PROGMEM stalledIteratorWrapperTemplate[] = "Start:%STALLING_LIST%:End"; + +struct StalledIteratorState { + size_t calls; +}; + +static StalledIteratorState stalledIteratorState; + +static void* stalledIteratorOpen(void* userData) { + StalledIteratorState* state = static_cast(userData); + if (state) { + state->calls = 0; + } + return state; +} + +static IteratorStepResult stalledIteratorNext(void* handle, IteratorItemView& view) { + StalledIteratorState* state = static_cast(handle); + if (state) { + state->calls++; + } + + view.templateData = nullptr; + view.templateLength = 0; + view.templateIsProgmem = false; + view.placeholders = nullptr; + view.placeholderCount = 0; + return IteratorStepResult::ITEM_READY; +} + +static void stalledIteratorClose(void* handle) { + (void)handle; +} + +static IteratorDescriptor stalledIteratorDescriptor = { + stalledIteratorOpen, + stalledIteratorNext, + stalledIteratorClose, + &stalledIteratorState +}; // Test TemplateRenderer basic rendering void test_template_renderer_basic() { @@ -1222,11 +1272,31 @@ void test_template_renderer_conditional_nested_iterator() { ctx.setRegistry(®istry); TemplateRenderer::initializeContext(ctx, iteratorConditionalWrapperTemplate); String rendered = captureRenderedOutput(ctx, 64); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Conditional iterator render should complete"); + TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "Conditional iterator render should not error"); TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("Net-A") >= 0, "Iterator content should appear inside conditional"); TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("Net-B") >= 0, "Iterator should render second item"); TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("Net-C") >= 0, "Iterator should render third item"); } +void test_template_renderer_conditional_template_delegate_completes() { + PlaceholderRegistry registry(8); + TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemTemplate("%COND_TMPL_TRUE%", conditionalTemplateTrueTemplate), "Conditional true template should register"); + TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemTemplate("%COND_TMPL_FALSE%", conditionalTemplateFalseTemplate), "Conditional false template should register"); + + conditionalState.result = ConditionalBranchResult::TRUE_BRANCH; + TEST_ASSERT_TRUE_MESSAGE(registry.registerConditional("%COND_TMPL_BLOCK%", &conditionalTemplateDescriptor), "Conditional template placeholder should register"); + + TemplateContext ctx; + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, conditionalTemplateWrapperTemplate); + String rendered = captureRenderedOutput(ctx, 8); + + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Conditional template delegate should complete"); + TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "Conditional template delegate should not error"); + TEST_ASSERT_EQUAL_STRING_MESSAGE("Cond:Delegate True:Done", rendered.c_str(), "Conditional template delegate should render full branch content"); +} + void test_template_renderer_iterator_basic() { PlaceholderRegistry registry(8); resetWifiIteratorState(3); @@ -1291,3 +1361,20 @@ void test_template_renderer_iterator_error_cleanup() { TEST_ASSERT_TRUE_MESSAGE(wifiIteratorState.closeCalled, "Iterator close should run on error"); } +void test_template_renderer_iterator_stall_guard() { + PlaceholderRegistry registry(4); + stalledIteratorState.calls = 0; + TEST_ASSERT_TRUE_MESSAGE(registry.registerIterator("%STALLING_LIST%", &stalledIteratorDescriptor), "Stalling iterator placeholder should register"); + + TemplateContext ctx; + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, stalledIteratorWrapperTemplate); + + uint8_t buffer[16]; + size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer)); + TEST_ASSERT_GREATER_THAN_MESSAGE(0u, static_cast(written), "Stalling iterator should emit static prefix before failing"); + TEST_ASSERT_EQUAL_MEMORY_MESSAGE("Start:", buffer, 6, "Stalling iterator output should include expected prefix"); + TEST_ASSERT_TRUE_MESSAGE(ctx.hasError(), "Renderer should enter error state after exhausting iteration guard"); + TEST_ASSERT_TRUE_MESSAGE(stalledIteratorState.calls > 0, "Stalling iterator should be advanced at least once"); +} +