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:
2026-04-16 13:48:25 +10:00
parent 416ab74238
commit 73fdd28f74
12 changed files with 262 additions and 38 deletions
+2
View File
@@ -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),
+2
View File
@@ -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(&registry);
// 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(&registry);
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(&registry);
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(&registry);
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(&registry);
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");
}