mirror of
https://github.com/alexhopeoconnor/DFTE.git
synced 2026-10-03 22:21:59 +10:00
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.
This commit is contained in:
@@ -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<PlaceholderRegistry> registry;
|
||||
void setupRegistry() {
|
||||
registry = std::make_shared<PlaceholderRegistry>();
|
||||
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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
#include "DeviceFrameworkPlaceholderRegistry.h"
|
||||
#include "DeviceFrameworkTemplateEngineDebug.h"
|
||||
#include <pgmspace.h>
|
||||
#include <new>
|
||||
|
||||
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<const char*>(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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<uint32_t>(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(),
|
||||
|
||||
@@ -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<uint32_t>(expectedLen),
|
||||
static_cast<uint32_t>(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];
|
||||
|
||||
@@ -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[] = "<ul>%WIFI_LIST%</ul>";
|
||||
static const char PROGMEM iteratorConditionalWrapperTemplate[] = "Badge:<ul>%WIFI_BADGE%</ul>";
|
||||
static const char PROGMEM conditionalTemplateWrapperTemplate[] = "Cond:%COND_TMPL_BLOCK%:Done";
|
||||
static const char PROGMEM conditionalTemplateTrueTemplate[] = "<span>Delegate True</span>";
|
||||
static const char PROGMEM conditionalTemplateFalseTemplate[] = "<span>Delegate False</span>";
|
||||
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<StalledIteratorState*>(userData);
|
||||
if (state) {
|
||||
state->calls = 0;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
static IteratorStepResult stalledIteratorNext(void* handle, IteratorItemView& view) {
|
||||
StalledIteratorState* state = static_cast<StalledIteratorState*>(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:<span>Delegate True</span>: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<uint32_t>(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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user