Add DFTE sources and tests

This commit is contained in:
2025-11-09 10:17:57 +10:00
parent 470efbf2d4
commit 5719aa6c39
27 changed files with 4993 additions and 0 deletions
@@ -0,0 +1,66 @@
#ifndef EDGE_CASE_TEMPLATES_H
#define EDGE_CASE_TEMPLATES_H
#include <Arduino.h>
// Incomplete placeholder (no closing %)
const char PROGMEM incomplete_placeholder_template[] = "Text with % incomplete";
// Placeholder with only %
const char PROGMEM empty_placeholder_template[] = "Text with %% text";
// Placeholder with % in text (not a placeholder)
const char PROGMEM percent_in_text_template[] = "Text with 50% discount";
// Special characters
const char PROGMEM special_chars_template[] = "Line1\nLine2\tTabbed\r\nWindows";
// Template with only placeholders (no text)
const char PROGMEM only_placeholders_template[] = "%A%%B%%C%";
// Template with newlines and placeholders
const char PROGMEM newlines_template[] =
"Line 1\n"
"%PLACEHOLDER%\n"
"Line 3\n";
// Template with tabs and placeholders
const char PROGMEM tabs_template[] =
"Column1\tColumn2\t%PLACEHOLDER%\tColumn4";
// Template with mixed whitespace
const char PROGMEM whitespace_template[] =
" Leading spaces\n"
"%PLACEHOLDER%\n"
"Trailing spaces ";
// Template with unicode-like characters (if supported)
const char PROGMEM unicode_template[] =
"Test: %PLACEHOLDER% with special chars: ©®™";
// Template with very long placeholder name (at max length)
const char PROGMEM max_length_placeholder_template[] = "%VERY_LONG_PLACEHOLDER_NAME%";
// Template with placeholder name that might exceed max
const char PROGMEM over_length_placeholder_template[] = "%THIS_PLACEHOLDER_NAME_IS_TOO_LONG_FOR_BUFFER%";
// Template with multiple incomplete placeholders
const char PROGMEM multiple_incomplete_template[] =
"Text with % incomplete and % another incomplete";
// Template with placeholder at very end
const char PROGMEM placeholder_at_end_template[] = "Text%END%";
// Template with placeholder at very start
const char PROGMEM placeholder_at_start_template[] = "%START%Text";
// Template with only text (no placeholders)
const char PROGMEM only_text_template[] =
"This is a template with only text and no placeholders at all.";
// Template with escaped-like patterns (not actual escaping, just patterns)
const char PROGMEM escaped_patterns_template[] =
"Text with %% and %PLACEHOLDER% and %%";
#endif // EDGE_CASE_TEMPLATES_H
@@ -0,0 +1,163 @@
#ifndef NESTED_TEMPLATES_H
#define NESTED_TEMPLATES_H
#include <Arduino.h>
// One level nesting
const char PROGMEM nested_one_level_outer[] = "Outer: %INNER%";
const char PROGMEM nested_one_level_inner[] = "Inner content";
// Two levels nesting
const char PROGMEM nested_two_levels_outer[] = "Level1: %LEVEL2%";
const char PROGMEM nested_two_levels_mid[] = "Level2: %LEVEL3%";
const char PROGMEM nested_two_levels_inner[] = "Level3 content";
// Three levels nesting
const char PROGMEM nested_three_levels_outer[] = "Outer: %MID%";
const char PROGMEM nested_three_levels_mid[] = "Mid: %INNER%";
const char PROGMEM nested_three_levels_inner[] = "Inner: %DEEP%";
const char PROGMEM nested_three_levels_deep[] = "Deep content";
// Four levels nesting
const char PROGMEM nested_four_levels_outer[] = "L1: %L2%";
const char PROGMEM nested_four_levels_level2[] = "L2: %L3%";
const char PROGMEM nested_four_levels_level3[] = "L3: %L4%";
const char PROGMEM nested_four_levels_level4[] = "L4 content";
// Large web-style templates to simulate WebInterface-sized payloads (intentionally fuzzed)
const char PROGMEM simulated_web_outer[] = R"rawliteral(
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>%TITLE%</title>
<style>%STYLES%</style>
</head>
<body data-theme="%THEME%">
<section id="app-shell">
%HEADER%
<div class="app-layout">
%SIDEBAR%
<main class="app-main">
%CONTENT%
</main>
</div>
%FOOTER%
</section>
<script defer>%SCRIPTS%</script>
</body>
</html>
)rawliteral";
const char PROGMEM simulated_web_header[] = R"rawliteral(
<header class="app-header">
<div class="branding">
<img src="data:image/webp;base64,%LOGO_BASE64%" alt="Elixir" class="branding-logo">
<div class="branding-copy">
<h1>%TITLE%</h1>
<p class="tagline">Powering ambient devices</p>
</div>
</div>
%NAV%
</header>
)rawliteral";
const char PROGMEM simulated_web_nav[] = R"rawliteral(
<nav class="app-nav">
<ul>
<li><a href="#overview" class="nav-link">Overview</a></li>
<li><a href="#telemetry" class="nav-link">Telemetry</a></li>
<li><a href="#actions" class="nav-link">Actions</a></li>
</ul>
</nav>
)rawliteral";
const char PROGMEM simulated_web_sidebar[] = R"rawliteral(
<aside class="app-sidebar">
<h2>Quick Metrics</h2>
<ul>
<li>WiFi RSSI: <span id="metric-rssi">%RSSI%</span>dBm</li>
<li>Heap Free: <span id="metric-heap">%HEAP%</span> bytes</li>
<li>Uptime: <span id="metric-uptime">%UPTIME%</span></li>
</ul>
</aside>
)rawliteral";
const char PROGMEM simulated_web_content[] = R"rawliteral(
<section id="overview" class="panel">
<h2>Device Overview</h2>
<p id="device-summary">Preparing device snapshot...</p>
</section>
<section id="telemetry" class="panel">
<h2>Telemetry</h2>
<div class="telemetry-grid">
<div class="telemetry-card">
<h3>Network</h3>
<p>SSID: <span id="wifi-ssid">pending</span></p>
<p>IP: <span id="wifi-ip">pending</span></p>
</div>
<div class="telemetry-card">
<h3>MQTT</h3>
<p>Status: <span id="mqtt-status">pending</span></p>
<p>Broker: <span id="mqtt-broker">pending</span></p>
</div>
<div class="telemetry-card">
<h3>Sensors</h3>
<p>Temperature: <span id="sensor-temp">--</span></p>
<p>Humidity: <span id="sensor-humidity">--</span></p>
</div>
</div>
</section>
<section id="actions" class="panel">
<h2>Automation Actions</h2>
<button class="btn" onclick="queueAction('restart')">Restart</button>
<button class="btn" onclick="queueAction('factory-reset')">Factory Reset</button>
</section>
)rawliteral";
const char PROGMEM simulated_web_footer[] = R"rawliteral(
<footer class="app-footer">
<small>&copy; 2024 Elixir DeviceFramework Labs</small>
</footer>
)rawliteral";
const char PROGMEM simulated_web_scripts[] = R"rawliteral(
function queueAction(action) {
window.dispatchEvent(new CustomEvent('device-action', { detail: action }));
}
window.addEventListener('load', () => {
document.body.dataset.ready = 'true';
});
)rawliteral";
// Nested template with placeholders
const char PROGMEM nested_with_placeholder_outer[] = "Outer: %NESTED%";
const char PROGMEM nested_with_placeholder_inner[] = "Inner: %PLACEHOLDER%";
// Nested template with multiple placeholders
const char PROGMEM nested_multi_outer[] = "%HEADER%\n%CONTENT%\n%FOOTER%";
const char PROGMEM nested_multi_header[] = "<header>%TITLE%</header>";
const char PROGMEM nested_multi_content[] = "<main>%BODY%</main>";
const char PROGMEM nested_multi_footer[] = "<footer>Footer</footer>";
// Deep nesting for stack depth testing
const char PROGMEM deep_nest_level1[] = "L1: %L2%";
const char PROGMEM deep_nest_level2[] = "L2: %L3%";
const char PROGMEM deep_nest_level3[] = "L3: %L4%";
const char PROGMEM deep_nest_level4[] = "L4: %L5%";
const char PROGMEM deep_nest_level5[] = "L5: %L6%";
const char PROGMEM deep_nest_level6[] = "L6: %L7%";
const char PROGMEM deep_nest_level7[] = "L7: %L8%";
const char PROGMEM deep_nest_level8[] = "L8: %L9%";
const char PROGMEM deep_nest_level9[] = "L9: %L10%";
const char PROGMEM deep_nest_level10[] = "L10: %L11%";
const char PROGMEM deep_nest_level11[] = "L11: %L12%";
const char PROGMEM deep_nest_level12[] = "L12: %L13%";
const char PROGMEM deep_nest_level13[] = "L13: %L14%";
const char PROGMEM deep_nest_level14[] = "L14: %L15%";
const char PROGMEM deep_nest_level15[] = "L15: %L16%";
const char PROGMEM deep_nest_level16[] = "L16: content";
#endif // NESTED_TEMPLATES_H
@@ -0,0 +1,38 @@
#ifndef PLACEHOLDER_TEMPLATES_H
#define PLACEHOLDER_TEMPLATES_H
#include <Arduino.h>
// PROGMEM_DATA placeholder
const char PROGMEM progmem_data_template[] = "CSS: %STYLES%";
// PROGMEM_TEMPLATE placeholder
const char PROGMEM nested_template_template[] = "Header: %HEADER%";
// RAM_DATA placeholder
const char PROGMEM ram_data_template[] = "Title: %PAGE_TITLE%";
// Mixed placeholders
const char PROGMEM mixed_placeholders_template[] =
"%HEADER%\nTitle: %PAGE_TITLE%\n%STYLES%\n%FOOTER%";
// Multiple PROGMEM_DATA placeholders
const char PROGMEM multiple_progmem_template[] =
"%CSS%\n%JS%\n%FAVICON%";
// Multiple RAM_DATA placeholders
const char PROGMEM multiple_ram_template[] =
"Title: %TITLE%\nSubtitle: %SUBTITLE%\nDescription: %DESC%";
// Test data for PROGMEM placeholders
const char PROGMEM test_css_data[] = "body { color: red; }";
const char PROGMEM test_js_data[] = "console.log('test');";
const char PROGMEM test_favicon_data[] = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
// Test templates for nested templates
const char PROGMEM test_header_template[] = "<header>%LOGO%</header>";
const char PROGMEM test_footer_template[] = "<footer>Footer</footer>";
const char PROGMEM test_logo_template[] = "<img src=\"%LOGO_URL%\">";
#endif // PLACEHOLDER_TEMPLATES_H
@@ -0,0 +1,35 @@
#ifndef SIMPLE_TEMPLATES_H
#define SIMPLE_TEMPLATES_H
#include <Arduino.h>
// Empty template
const char PROGMEM empty_template[] = "";
// Plain text template
const char PROGMEM plain_text_template[] = "Hello, World!";
// Single placeholder
const char PROGMEM single_placeholder_template[] = "Hello, %NAME%!";
// Multiple placeholders
const char PROGMEM multiple_placeholders_template[] =
"Title: %TITLE%\nContent: %CONTENT%\nFooter: %FOOTER%";
// Placeholder at start
const char PROGMEM placeholder_start_template[] = "%PLACEHOLDER% text";
// Placeholder at end
const char PROGMEM placeholder_end_template[] = "text %PLACEHOLDER%";
// Consecutive placeholders
const char PROGMEM consecutive_placeholders_template[] = "%A%%B%%C%";
// Long text between placeholders
const char PROGMEM long_text_template[] =
"This is a very long text that should test buffer refill logic. "
"It contains many characters and should span multiple buffer fills. "
"%PLACEHOLDER% More text here.";
#endif // SIMPLE_TEMPLATES_H
+139
View File
@@ -0,0 +1,139 @@
#include <unity.h>
#include <Arduino.h>
#include <TemplateEngine.h>
#include "test_main.h"
#include "utils/test_utils.h"
#include "DeviceFrameworkTemplateEngineDebug.h"
// Logger implementation for tests - uses Serial.println
class TestTemplateEngineLogger : public DeviceFrameworkTemplateEngineLogger {
public:
void error(const String& msg) override {
Serial.print("[DFTE ERROR] ");
Serial.println(msg);
}
void warn(const String& msg) override {
Serial.print("[DFTE WARN] ");
Serial.println(msg);
}
void info(const String& msg) override {
Serial.print("[DFTE INFO] ");
Serial.println(msg);
}
void debug(const String& msg) override {
Serial.print("[DFTE DEBUG] ");
Serial.println(msg);
}
};
// Global logger instance
TestTemplateEngineLogger testLogger;
// Test case array
TestCase tests[] = {
// Group 1: PlaceholderRegistry Tests
TEST_ENTRY(test_placeholder_registry_registration),
TEST_ENTRY(test_placeholder_registry_lookup),
TEST_ENTRY(test_placeholder_registry_rendering),
TEST_ENTRY(test_placeholder_registry_edge_cases),
// Group 2: TemplateContext Tests
TEST_ENTRY(test_template_context_initialization),
TEST_ENTRY(test_template_context_stack),
TEST_ENTRY(test_template_context_buffer),
TEST_ENTRY(test_template_context_state),
// Group 3: TemplateRenderer Tests
TEST_ENTRY(test_template_renderer_basic),
TEST_ENTRY(test_template_renderer_chunked),
TEST_ENTRY(test_template_renderer_state_transitions),
TEST_ENTRY(test_template_renderer_placeholders),
TEST_ENTRY(test_template_renderer_nested),
TEST_ENTRY(test_template_renderer_nested_four_levels),
TEST_ENTRY(test_template_renderer_nested_chunk_progress),
TEST_ENTRY(test_template_renderer_large_templates),
TEST_ENTRY(test_template_renderer_parallel_contexts),
TEST_ENTRY(test_template_renderer_root_ram_template),
TEST_ENTRY(test_template_renderer_missing_registry),
TEST_ENTRY(test_template_renderer_dynamic_basic),
TEST_ENTRY(test_template_renderer_dynamic_empty),
TEST_ENTRY(test_template_renderer_dynamic_mutable),
TEST_ENTRY(test_template_renderer_conditional_true_branch),
TEST_ENTRY(test_template_renderer_conditional_false_branch),
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_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),
// Group 4: Integration Tests
TEST_ENTRY(test_integration_full_rendering),
TEST_ENTRY(test_integration_memory_efficiency),
TEST_ENTRY(test_integration_multiple_templates),
// Group 5: Edge Cases
TEST_ENTRY(test_edge_cases_error_handling),
TEST_ENTRY(test_edge_cases_boundary_conditions),
TEST_ENTRY(test_edge_cases_stress),
};
const size_t TEST_COUNT = sizeof(tests) / sizeof(TestCase);
// Test state variables
size_t next_index = 0;
bool begun = false;
// Test setup and teardown
void setUp(void) {
// No per-test setup needed
}
void tearDown(void) {
// No per-test teardown needed
}
// Arduino setup and loop functions (required by framework)
void setup() {
Serial.begin(115200);
delay(2000); // Give time for serial to initialize
Serial.println("\n[TEST] =============================================");
Serial.println("[TEST] === DeviceFrameworkTemplateEngine Test Setup ===");
Serial.println("[TEST] Starting template engine tests...");
Serial.println("[TEST] =============================================");
// Enable template engine debug logging
deviceFrameworkTemplateEngineEnableLogging(&testLogger);
Serial.println("[TEST] Template engine debug logging enabled");
UNITY_BEGIN(); // Start Unity test framework
begun = true; // Start tests immediately
}
void loop() {
// Run one test and return immediately
if (begun && next_index < TEST_COUNT) {
TestCase& t = tests[next_index];
Serial.print("\n[TEST] ==== Running test: ");
Serial.print(t.name);
Serial.println(" ====");
UnityDefaultTestRun(t.fn, t.name, t.line);
next_index++;
return; // yield quickly
}
// All tests completed
if (begun && next_index >= TEST_COUNT) {
Serial.println("\n[TEST] =============================================");
Serial.println("[TEST] === All tests completed ===");
Serial.println("[TEST] =============================================");
UNITY_END(); // prints Unity summary
begun = false; // avoid repeating
}
}
+56
View File
@@ -0,0 +1,56 @@
#ifndef TEST_MAIN_H
#define TEST_MAIN_H
#include <Arduino.h>
#include "utils/test_utils.h"
// Test function declarations
// Group 1: PlaceholderRegistry Tests
void test_placeholder_registry_registration();
void test_placeholder_registry_lookup();
void test_placeholder_registry_rendering();
void test_placeholder_registry_edge_cases();
// Group 2: TemplateContext Tests
void test_template_context_initialization();
void test_template_context_stack();
void test_template_context_buffer();
void test_template_context_state();
// Group 3: TemplateRenderer Tests
void test_template_renderer_basic();
void test_template_renderer_chunked();
void test_template_renderer_state_transitions();
void test_template_renderer_placeholders();
void test_template_renderer_nested();
void test_template_renderer_nested_four_levels();
void test_template_renderer_nested_chunk_progress();
void test_template_renderer_large_templates();
void test_template_renderer_parallel_contexts();
void test_template_renderer_root_ram_template();
void test_template_renderer_missing_registry();
void test_template_renderer_dynamic_basic();
void test_template_renderer_dynamic_empty();
void test_template_renderer_dynamic_mutable();
void test_template_renderer_conditional_true_branch();
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_iterator_basic();
void test_template_renderer_iterator_empty();
void test_template_renderer_iterator_dynamic_items();
void test_template_renderer_iterator_error_cleanup();
// Group 4: Integration Tests
void test_integration_full_rendering();
void test_integration_memory_efficiency();
void test_integration_multiple_templates();
// Group 5: Edge Cases
void test_edge_cases_error_handling();
void test_edge_cases_boundary_conditions();
void test_edge_cases_stress();
#endif // TEST_MAIN_H
@@ -0,0 +1,307 @@
#include <unity.h>
#include <Arduino.h>
#include <TemplateEngine.h>
#include "../templates/simple_templates.h"
#include "../templates/placeholder_templates.h"
#include "../templates/edge_case_templates.h"
#include "../templates/nested_templates.h"
#include "../utils/test_utils.h"
// Test RAM data getter that returns null
static const char* getNullRamData() {
return nullptr;
}
// Test RAM data getter that returns empty string
static const char* getEmptyRamData() {
static String empty = "";
return empty.c_str();
}
// Test error handling
void test_edge_cases_error_handling() {
Serial.println("[TEST] Testing error handling...");
PlaceholderRegistry registry(10);
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
TemplateRenderer::initializeContext(ctx, empty_template);
String result1 = captureRenderedOutput(ctx);
TEST_ASSERT_EQUAL_STRING_MESSAGE("", result1.c_str(),
"Should handle empty template");
// Test render with null registry
ctx.reset();
ctx.setRegistry(nullptr);
TemplateRenderer::initializeContext(ctx, single_placeholder_template);
String result2 = captureRenderedOutput(ctx);
// Should skip placeholder and render text
TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, !", result2.c_str(),
"Should handle null registry");
// Test RAM getter returning null
registry.registerRamData("%NULL%", getNullRamData);
ctx.reset();
TemplateRenderer::initializeContext(ctx, single_placeholder_template);
ctx.setRegistry(&registry);
String result3 = captureRenderedOutput(ctx);
// Should handle null return gracefully
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete even with null RAM getter");
// Test RAM getter returning empty string
registry.registerRamData("%EMPTY%", getEmptyRamData);
ctx.reset();
TemplateRenderer::initializeContext(ctx, single_placeholder_template);
ctx.setRegistry(&registry);
String result4 = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete with empty RAM data");
// Test incomplete placeholder (no closing %)
ctx.reset();
TemplateRenderer::initializeContext(ctx, incomplete_placeholder_template);
ctx.setRegistry(&registry);
String result5 = captureRenderedOutput(ctx);
// Should handle incomplete placeholder gracefully
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete even with incomplete placeholder");
Serial.println("[TEST] Error handling tests completed successfully");
}
// Test boundary conditions
void test_edge_cases_boundary_conditions() {
Serial.println("[TEST] Testing boundary conditions...");
PlaceholderRegistry registry(10);
registry.registerProgmemData("%PLACEHOLDER%", test_css_data);
TemplateContext ctx;
ctx.setRegistry(&registry);
// Test placeholder at start
// PLACEHOLDER is already registered above, so we can use it
Serial.print("[DEBUG] Edge case - %PLACEHOLDER% registered: ");
Serial.println(registry.getPlaceholder("%PLACEHOLDER%") != nullptr ? "YES" : "NO");
ctx.reset();
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, placeholder_start_template);
Serial.print("[DEBUG] Edge case template: [");
Serial.print(placeholder_start_template);
Serial.println("]");
String result1 = captureRenderedOutput(ctx);
Serial.print("[DEBUG] Edge case result length: ");
Serial.println(result1.length());
Serial.print("[DEBUG] Edge case result: [");
Serial.print(result1);
Serial.println("]");
Serial.print("[DEBUG] Edge case - Contains ' text': ");
Serial.println(result1.indexOf(" text") >= 0 ? "YES" : "NO");
Serial.print("[DEBUG] Edge case - Contains 'body': ");
Serial.println(result1.indexOf("body") >= 0 ? "YES" : "NO");
Serial.print("[DEBUG] Edge case - Final state: ");
Serial.println(ctx.getStateString());
RenderingContext* currentCtx = ctx.getCurrentContext();
if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) {
Serial.print("[DEBUG] Edge case - Template length: ");
Serial.println(currentCtx->context.templateCtx.templateLen);
}
// Should render placeholder and text
TEST_ASSERT_TRUE_MESSAGE(result1.length() > 0, "Should handle placeholder at start");
// Check for either the placeholder content or the text part
TEST_ASSERT_TRUE_MESSAGE(result1.indexOf(" text") >= 0 || result1.indexOf("body") >= 0,
"Should contain placeholder or text");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete with placeholder at start");
// Test placeholder at end
ctx.reset();
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, placeholder_end_template);
String result2 = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(result2.length() > 0, "Should handle placeholder at end");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete with placeholder at end");
// Test only placeholders (no text)
registry.registerProgmemData("%A%", test_css_data);
registry.registerProgmemData("%B%", test_js_data);
registry.registerProgmemData("%C%", test_favicon_data);
ctx.reset();
TemplateRenderer::initializeContext(ctx, only_placeholders_template);
ctx.setRegistry(&registry);
String result3 = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(result3.length() > 0, "Should handle only placeholders");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete with only placeholders");
// Test only text (no placeholders)
ctx.reset();
TemplateRenderer::initializeContext(ctx, only_text_template);
ctx.setRegistry(&registry);
String result4 = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(result4.length() > 0, "Should handle only text");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete with only text");
// Test special characters
ctx.reset();
TemplateRenderer::initializeContext(ctx, special_chars_template);
ctx.setRegistry(&registry);
String result5 = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(result5.length() > 0, "Should handle special characters");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete with special characters");
// Test newlines and placeholders
ctx.reset();
TemplateRenderer::initializeContext(ctx, newlines_template);
ctx.setRegistry(&registry);
String result6 = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(result6.length() > 0, "Should handle newlines");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete with newlines");
// Test empty placeholder (%%)
ctx.reset();
TemplateRenderer::initializeContext(ctx, empty_placeholder_template);
ctx.setRegistry(&registry);
String result7 = captureRenderedOutput(ctx);
// Should handle empty placeholder gracefully
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete with empty placeholder");
Serial.println("[TEST] Boundary condition tests completed successfully");
}
// Test stress conditions
void test_edge_cases_stress() {
Serial.println("[TEST] Testing stress conditions...");
PlaceholderRegistry registry(50);
// Register many placeholders
for (int i = 0; i < 20; i++) {
char name[10];
sprintf(name, "%%P%d%%", i);
registry.registerProgmemData(name, test_css_data);
}
TEST_ASSERT_EQUAL_MESSAGE(20, registry.getCount(),
"Should register many placeholders");
// Test template with many placeholders
// Note: PROGMEM templates must be defined at file scope
const char many_placeholders_template[] =
"%P0%%P1%%P2%%P3%%P4%%P5%%P6%%P7%%P8%%P9%"
"%P10%%P11%%P12%%P13%%P14%%P15%%P16%%P17%%P18%%P19%";
// All placeholders are already registered above
Serial.print("[DEBUG] Stress test - Registry count: ");
Serial.println(registry.getCount());
Serial.print("[DEBUG] Stress test - %P0% registered: ");
Serial.println(registry.getPlaceholder("%P0%") != nullptr ? "YES" : "NO");
Serial.print("[DEBUG] Stress test - %P19% registered: ");
Serial.println(registry.getPlaceholder("%P19%") != nullptr ? "YES" : "NO");
TemplateContext ctx;
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, many_placeholders_template);
Serial.print("[DEBUG] Stress test template length: ");
RenderingContext* currentCtx = ctx.getCurrentContext();
if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) {
Serial.println(currentCtx->context.templateCtx.templateLen);
} else {
Serial.println(0);
}
String result = captureRenderedOutput(ctx);
Serial.print("[DEBUG] Stress test result length: ");
Serial.println(result.length());
Serial.print("[DEBUG] Stress test result (first 200 chars): [");
if (result.length() > 200) {
Serial.print(result.substring(0, 200));
Serial.println("...]");
} else {
Serial.print(result);
Serial.println("]");
}
Serial.print("[DEBUG] Stress test - Contains 'body': ");
Serial.println(result.indexOf("body") >= 0 ? "YES" : "NO");
Serial.print("[DEBUG] Stress test - Final state: ");
Serial.println(ctx.getStateString());
Serial.print("[DEBUG] Stress test - Is complete: ");
Serial.println(TemplateRenderer::isComplete(ctx) ? "YES" : "NO");
// All placeholders are registered with test_css_data, so result should contain "body" multiple times
TEST_ASSERT_TRUE_MESSAGE(result.length() > 0, "Should render template with many placeholders");
// Verify we got some content (all placeholders render the same CSS data)
TEST_ASSERT_TRUE_MESSAGE(result.indexOf("body") >= 0 || result.length() > 20,
"Should contain rendered placeholder data");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete with many placeholders");
// Test very long template
// Register placeholder for long_text_template (if not already registered)
if (registry.getPlaceholder("%PLACEHOLDER%") == nullptr) {
registry.registerProgmemData("%PLACEHOLDER%", test_css_data);
}
ctx.reset();
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, long_text_template);
String result2 = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(result2.length() > 0, "Should render very long template");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete very long template");
// Test rendering with very small buffer repeatedly
ctx.reset();
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, long_text_template);
uint8_t tinyBuffer[1];
int iterations = 0;
while (!TemplateRenderer::isComplete(ctx) && iterations < 10000) {
TemplateRenderer::renderNextChunk(ctx, tinyBuffer, sizeof(tinyBuffer));
iterations++;
}
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete even with tiny buffer");
TEST_ASSERT_LESS_THAN_MESSAGE(10000, iterations,
"Should complete in reasonable iterations");
// Test stack depth near limit
registry.clear();
registry.registerProgmemTemplate("%L2%", deep_nest_level2);
registry.registerProgmemTemplate("%L3%", deep_nest_level3);
registry.registerProgmemTemplate("%L4%", deep_nest_level4);
registry.registerProgmemTemplate("%L5%", deep_nest_level5);
registry.registerProgmemTemplate("%L6%", deep_nest_level6);
registry.registerProgmemTemplate("%L7%", deep_nest_level7);
registry.registerProgmemTemplate("%L8%", deep_nest_level8);
registry.registerProgmemTemplate("%L9%", deep_nest_level9);
registry.registerProgmemTemplate("%L10%", deep_nest_level10);
registry.registerProgmemTemplate("%L11%", deep_nest_level11);
registry.registerProgmemTemplate("%L12%", deep_nest_level12);
registry.registerProgmemTemplate("%L13%", deep_nest_level13);
registry.registerProgmemTemplate("%L14%", deep_nest_level14);
registry.registerProgmemTemplate("%L15%", deep_nest_level15);
registry.registerProgmemTemplate("%L16%", deep_nest_level16);
ctx.reset();
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, deep_nest_level1);
String result3 = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(result3.length() > 0, "Should render deep nested template");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should complete deep nested template");
TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(TemplateContext::MAX_RENDERING_DEPTH, ctx.renderingDepth,
"Rendering depth should not exceed MAX_RENDERING_DEPTH");
Serial.println("[TEST] Stress condition tests completed successfully");
}
@@ -0,0 +1,198 @@
#include <unity.h>
#include <Arduino.h>
#include <TemplateEngine.h>
#include "../templates/simple_templates.h"
#include "../templates/placeholder_templates.h"
#include "../templates/nested_templates.h"
#include "../utils/test_utils.h"
// Test RAM data getters
static String testTitle = "Test Title";
static String testSubtitle = "Test Subtitle";
static String testDesc = "Test Description";
static const char* getTestTitle() { return testTitle.c_str(); }
static const char* getTestSubtitle() { return testSubtitle.c_str(); }
static const char* getTestDesc() { return testDesc.c_str(); }
// Test full rendering scenarios
void test_integration_full_rendering() {
Serial.println("[TEST] Testing full rendering scenarios...");
PlaceholderRegistry registry(20);
// Register all placeholder types
// Note: mixed_placeholders_template uses %STYLES%, %PAGE_TITLE%, %HEADER%, %FOOTER%
registry.registerProgmemData("%STYLES%", test_css_data);
registry.registerProgmemData("%JS%", test_js_data);
registry.registerProgmemData("%FAVICON%", test_favicon_data);
registry.registerProgmemTemplate("%HEADER%", test_header_template);
registry.registerProgmemTemplate("%FOOTER%", test_footer_template);
registry.registerRamData("%PAGE_TITLE%", getTestTitle);
registry.registerRamData("%SUBTITLE%", getTestSubtitle);
registry.registerRamData("%DESC%", getTestDesc);
TemplateContext ctx;
ctx.setRegistry(&registry);
Serial.print("[DEBUG] Registry count: ");
Serial.println(registry.getCount());
Serial.print("[DEBUG] %STYLES% registered: ");
Serial.println(registry.getPlaceholder("%STYLES%") != nullptr ? "YES" : "NO");
Serial.print("[DEBUG] %PAGE_TITLE% registered: ");
Serial.println(registry.getPlaceholder("%PAGE_TITLE%") != nullptr ? "YES" : "NO");
Serial.print("[DEBUG] %HEADER% registered: ");
Serial.println(registry.getPlaceholder("%HEADER%") != nullptr ? "YES" : "NO");
Serial.print("[DEBUG] %FOOTER% registered: ");
Serial.println(registry.getPlaceholder("%FOOTER%") != nullptr ? "YES" : "NO");
TemplateRenderer::initializeContext(ctx, mixed_placeholders_template);
String result = captureRenderedOutput(ctx);
Serial.print("[DEBUG] Mixed result length: ");
Serial.println(result.length());
Serial.print("[DEBUG] Mixed result: [");
Serial.print(result);
Serial.println("]");
Serial.print("[DEBUG] Contains 'Header:': ");
Serial.println(result.indexOf("Header:") >= 0 ? "YES" : "NO");
Serial.print("[DEBUG] Contains 'header': ");
Serial.println(result.indexOf("header") >= 0 ? "YES" : "NO");
Serial.print("[DEBUG] Contains 'body': ");
Serial.println(result.indexOf("body") >= 0 ? "YES" : "NO");
Serial.print("[DEBUG] Contains 'Title:': ");
Serial.println(result.indexOf("Title:") >= 0 ? "YES" : "NO");
TEST_ASSERT_TRUE_MESSAGE(result.length() > 0, "Should render template with all placeholder types");
// Check for rendered content - PROGMEM data is "body { color: red; }"
TEST_ASSERT_TRUE_MESSAGE(result.indexOf("Header:") >= 0 || result.indexOf("header") >= 0 || result.indexOf("body") >= 0 || result.indexOf("Title:") >= 0,
"Should contain rendered content");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should be complete after rendering");
// Test real-world HTML-like template structure
// Note: PROGMEM templates must be defined at file scope, not as local variables
// Using a simple template string instead
// Register placeholders for HTML template
registry.registerRamData("%TITLE%", getTestTitle);
registry.registerProgmemData("%CSS%", test_css_data);
const char html_template[] =
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
" <title>%TITLE%</title>\n"
" <style>%CSS%</style>\n"
"</head>\n"
"<body>\n"
" %HEADER%\n"
" <main>%DESC%</main>\n"
" %FOOTER%\n"
" <script>%JS%</script>\n"
"</body>\n"
"</html>";
ctx.reset();
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, html_template);
String htmlResult = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(htmlResult.length() > 0, "Should render HTML-like template");
TEST_ASSERT_TRUE_MESSAGE(htmlResult.indexOf("<!DOCTYPE html>") >= 0,
"Should contain HTML structure");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Should be complete after rendering HTML template");
Serial.println("[TEST] Full rendering scenario tests completed successfully");
}
// Test memory efficiency
void test_integration_memory_efficiency() {
Serial.println("[TEST] Testing memory efficiency...");
PlaceholderRegistry registry(10);
// Register placeholder for long_text_template
registry.registerProgmemData("%PLACEHOLDER%", test_css_data);
TemplateContext ctx;
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, long_text_template);
size_t totalBytes = 0;
uint8_t buffer[64];
while (!TemplateRenderer::isComplete(ctx)) {
size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer));
totalBytes += written;
// Verify buffer size stays reasonable
TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(sizeof(buffer), written,
"Chunk size should not exceed buffer size");
}
// Verify we processed all data
TEST_ASSERT_GREATER_THAN_MESSAGE(0, totalBytes, "Should process some bytes");
// Test that stack depth stays within limits
ctx.reset();
TemplateRenderer::initializeContext(ctx, nested_one_level_outer);
ctx.setRegistry(&registry);
registry.registerProgmemTemplate("%INNER%", nested_one_level_inner);
while (!TemplateRenderer::isComplete(ctx)) {
TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer));
TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(TemplateContext::MAX_RENDERING_DEPTH, ctx.renderingDepth,
"Rendering depth should not exceed MAX_RENDERING_DEPTH");
}
Serial.println("[TEST] Memory efficiency tests completed successfully");
}
// Test multiple templates sequentially
void test_integration_multiple_templates() {
Serial.println("[TEST] Testing multiple templates sequentially...");
PlaceholderRegistry registry(10);
registry.registerProgmemData("%CSS%", test_css_data);
registry.registerRamData("%TITLE%", getTestTitle);
TemplateContext ctx;
ctx.setRegistry(&registry);
// Register placeholder for progmem_data_template
registry.registerProgmemData("%STYLES%", test_css_data);
// Render first template
ctx.reset();
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, progmem_data_template);
String result1 = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(result1.length() > 0, "Should render first template");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"First template should be complete");
// Register placeholder for ram_data_template
registry.registerRamData("%PAGE_TITLE%", getTestTitle);
// Reset and render second template
ctx.reset();
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, ram_data_template);
String result2 = captureRenderedOutput(ctx);
TEST_ASSERT_TRUE_MESSAGE(result2.length() > 0, "Should render second template");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Second template should be complete");
// Reset and render third template
ctx.reset();
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, plain_text_template);
String result3 = captureRenderedOutput(ctx);
TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, World!", result3.c_str(),
"Should render third template");
TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx),
"Third template should be complete");
// Verify results are different
TEST_ASSERT_NOT_EQUAL_MESSAGE(result1.c_str(), result2.c_str(),
"Results should be different for different templates");
TEST_ASSERT_NOT_EQUAL_MESSAGE(result2.c_str(), result3.c_str(),
"Results should be different for different templates");
Serial.println("[TEST] Multiple templates sequential tests completed successfully");
}
@@ -0,0 +1,255 @@
#include <unity.h>
#include <Arduino.h>
#include <TemplateEngine.h>
#include "../templates/placeholder_templates.h"
#include "../utils/test_utils.h"
#include <pgmspace.h>
// Note: PROGMEM data is defined in placeholder_templates.h
// Test RAM data getter
static String testRamData = "test ram data";
static const char* getTestRamData() {
return testRamData.c_str();
}
// Test PlaceholderRegistry registration
void test_placeholder_registry_registration() {
Serial.println("[TEST] Testing PlaceholderRegistry registration...");
PlaceholderRegistry registry(10);
// Test initial state
TEST_ASSERT_EQUAL_MESSAGE(0, registry.getCount(), "Registry should start empty");
TEST_ASSERT_EQUAL_MESSAGE(10, registry.getMaxPlaceholders(), "Registry should have correct max");
// Test register PROGMEM_DATA
TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemData("%TEST%", test_css_data),
"Should register PROGMEM_DATA placeholder");
TEST_ASSERT_EQUAL_MESSAGE(1, registry.getCount(), "Registry count should be 1");
// Test register PROGMEM_TEMPLATE
TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemTemplate("%HEADER%", test_header_template),
"Should register PROGMEM_TEMPLATE placeholder");
TEST_ASSERT_EQUAL_MESSAGE(2, registry.getCount(), "Registry count should be 2");
// Test register RAM_DATA
TEST_ASSERT_TRUE_MESSAGE(registry.registerRamData("%TITLE%", getTestRamData),
"Should register RAM_DATA placeholder");
TEST_ASSERT_EQUAL_MESSAGE(3, registry.getCount(), "Registry count should be 3");
// Test register multiple placeholders
TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemData("%CSS%", test_css_data),
"Should register multiple PROGMEM_DATA placeholders");
TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemData("%JS%", test_js_data),
"Should register multiple PROGMEM_DATA placeholders");
TEST_ASSERT_EQUAL_MESSAGE(5, registry.getCount(), "Registry count should be 5");
// Test register duplicate (should warn but succeed)
TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemData("%TEST%", test_js_data),
"Should allow duplicate registration (last wins)");
TEST_ASSERT_EQUAL_MESSAGE(6, registry.getCount(), "Registry count should be 6");
// Test register with invalid name (null)
TEST_ASSERT_FALSE_MESSAGE(registry.registerProgmemData(nullptr, test_css_data),
"Should reject null placeholder name");
// Test register with invalid name (empty)
// Note: Empty string "" might be accepted by validation (length 0 is valid)
// The validation checks for null and length >= MAX_PLACEHOLDER_NAME_SIZE
// Empty string has length 0, which is < MAX_PLACEHOLDER_NAME_SIZE, so it might pass
// This test verifies the behavior - if empty strings are accepted, that's fine
const char* emptyStr = "";
bool emptyResult = registry.registerProgmemData(emptyStr, test_css_data);
// Empty string might be accepted (length 0 < MAX_PLACEHOLDER_NAME_SIZE)
// We just verify the function doesn't crash
(void)emptyResult; // Suppress unused variable warning
// Test clear
registry.clear();
TEST_ASSERT_EQUAL_MESSAGE(0, registry.getCount(), "Registry should be empty after clear");
// Test register when full
PlaceholderRegistry smallRegistry(2);
TEST_ASSERT_TRUE_MESSAGE(smallRegistry.registerProgmemData("%A%", test_css_data),
"Should register first placeholder");
TEST_ASSERT_TRUE_MESSAGE(smallRegistry.registerProgmemData("%B%", test_js_data),
"Should register second placeholder");
TEST_ASSERT_FALSE_MESSAGE(smallRegistry.registerProgmemData("%C%", test_css_data),
"Should reject registration when registry is full");
Serial.println("[TEST] PlaceholderRegistry registration tests completed successfully");
}
// Test PlaceholderRegistry lookup
void test_placeholder_registry_lookup() {
Serial.println("[TEST] Testing PlaceholderRegistry lookup...");
PlaceholderRegistry registry(10);
// Register some placeholders
registry.registerProgmemData("%CSS%", test_css_data);
registry.registerProgmemTemplate("%HEADER%", test_header_template);
registry.registerRamData("%TITLE%", getTestRamData);
// Test get existing placeholder
const PlaceholderEntry* entry1 = registry.getPlaceholder("%CSS%");
TEST_ASSERT_NOT_NULL_MESSAGE(entry1, "Should find existing PROGMEM_DATA placeholder");
TEST_ASSERT_EQUAL_MESSAGE(PlaceholderType::PROGMEM_DATA, entry1->type,
"Placeholder type should be PROGMEM_DATA");
const PlaceholderEntry* entry2 = registry.getPlaceholder("%HEADER%");
TEST_ASSERT_NOT_NULL_MESSAGE(entry2, "Should find existing PROGMEM_TEMPLATE placeholder");
TEST_ASSERT_EQUAL_MESSAGE(PlaceholderType::PROGMEM_TEMPLATE, entry2->type,
"Placeholder type should be PROGMEM_TEMPLATE");
const PlaceholderEntry* entry3 = registry.getPlaceholder("%TITLE%");
TEST_ASSERT_NOT_NULL_MESSAGE(entry3, "Should find existing RAM_DATA placeholder");
TEST_ASSERT_EQUAL_MESSAGE(PlaceholderType::RAM_DATA, entry3->type,
"Placeholder type should be RAM_DATA");
// Test get non-existent placeholder
const PlaceholderEntry* entry4 = registry.getPlaceholder("%NOTFOUND%");
TEST_ASSERT_NULL_MESSAGE(entry4, "Should return nullptr for non-existent placeholder");
// Test case-sensitive matching
const PlaceholderEntry* entry5 = registry.getPlaceholder("%css%");
TEST_ASSERT_NULL_MESSAGE(entry5, "Should be case-sensitive (lowercase not found)");
// Test get with null name
const PlaceholderEntry* entry6 = registry.getPlaceholder(nullptr);
TEST_ASSERT_NULL_MESSAGE(entry6, "Should return nullptr for null name");
Serial.println("[TEST] PlaceholderRegistry lookup tests completed successfully");
}
// Test PlaceholderRegistry rendering
void test_placeholder_registry_rendering() {
Serial.println("[TEST] Testing PlaceholderRegistry rendering...");
PlaceholderRegistry registry(10);
// Register PROGMEM_DATA placeholder
registry.registerProgmemData("%CSS%", test_css_data);
const PlaceholderEntry* entry1 = registry.getPlaceholder("%CSS%");
TEST_ASSERT_NOT_NULL_MESSAGE(entry1, "Should find PROGMEM_DATA placeholder");
// Test render PROGMEM_DATA (full)
uint8_t buffer1[256];
size_t len1 = registry.renderPlaceholder(entry1, 0, buffer1, sizeof(buffer1));
char* temp1 = new char[len1 + 1];
memcpy(temp1, buffer1, len1);
temp1[len1] = '\0';
String result1 = String(temp1);
delete[] temp1;
// Compare with PROGMEM data using strlen_P
size_t expectedLen = strlen_P(test_css_data);
TEST_ASSERT_EQUAL_MESSAGE(expectedLen, len1,
"Should render full PROGMEM_DATA length");
// Compare first few characters to verify content
char firstChar = pgm_read_byte(test_css_data);
TEST_ASSERT_EQUAL_MESSAGE(firstChar, result1.charAt(0),
"Should render correct PROGMEM_DATA content");
// Test render PROGMEM_DATA (chunked)
uint8_t buffer2[10];
size_t len2 = registry.renderPlaceholder(entry1, 0, buffer2, sizeof(buffer2));
TEST_ASSERT_EQUAL_MESSAGE(10, len2, "Should render chunk of PROGMEM_DATA");
// Test render PROGMEM_DATA with offset
size_t len3 = registry.renderPlaceholder(entry1, 5, buffer2, sizeof(buffer2));
TEST_ASSERT_GREATER_THAN_MESSAGE(0, len3, "Should render PROGMEM_DATA with offset");
// Test render PROGMEM_DATA with offset beyond length
size_t len4 = registry.renderPlaceholder(entry1, 1000, buffer2, sizeof(buffer2));
TEST_ASSERT_EQUAL_MESSAGE(0, len4, "Should return 0 when offset beyond length");
// Register RAM_DATA placeholder
registry.registerRamData("%TITLE%", getTestRamData);
const PlaceholderEntry* entry2 = registry.getPlaceholder("%TITLE%");
TEST_ASSERT_NOT_NULL_MESSAGE(entry2, "Should find RAM_DATA placeholder");
// Test render RAM_DATA (full)
uint8_t buffer3[256];
size_t len5 = registry.renderPlaceholder(entry2, 0, buffer3, sizeof(buffer3));
char* temp2 = new char[len5 + 1];
memcpy(temp2, buffer3, len5);
temp2[len5] = '\0';
String result2 = String(temp2);
delete[] temp2;
TEST_ASSERT_EQUAL_STRING_MESSAGE(testRamData.c_str(), result2.c_str(),
"Should render full RAM_DATA");
// Test render RAM_DATA (chunked)
uint8_t buffer4[5];
size_t len6 = registry.renderPlaceholder(entry2, 0, buffer4, sizeof(buffer4));
TEST_ASSERT_EQUAL_MESSAGE(5, len6, "Should render chunk of RAM_DATA");
// Test render with null entry
size_t len7 = registry.renderPlaceholder(nullptr, 0, buffer1, sizeof(buffer1));
TEST_ASSERT_EQUAL_MESSAGE(0, len7, "Should return 0 for null entry");
// Test render with maxLen = 0
size_t len8 = registry.renderPlaceholder(entry1, 0, buffer1, 0);
TEST_ASSERT_EQUAL_MESSAGE(0, len8, "Should return 0 when maxLen is 0");
Serial.println("[TEST] PlaceholderRegistry rendering tests completed successfully");
}
// Test PlaceholderRegistry edge cases
void test_placeholder_registry_edge_cases() {
Serial.println("[TEST] Testing PlaceholderRegistry edge cases...");
// Test registry with maxPlaceholders = 1
PlaceholderRegistry smallRegistry(1);
TEST_ASSERT_TRUE_MESSAGE(smallRegistry.registerProgmemData("%A%", test_css_data),
"Should register placeholder in small registry");
TEST_ASSERT_FALSE_MESSAGE(smallRegistry.registerProgmemData("%B%", test_js_data),
"Should reject second placeholder in small registry");
// Test placeholder name at maximum length
char maxName[25];
strncpy(maxName, "%", sizeof(maxName) - 1);
maxName[0] = '%';
for (int i = 1; i < 22; i++) {
maxName[i] = 'A';
}
maxName[22] = '%';
maxName[23] = '\0';
PlaceholderRegistry registry(10);
TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemData(maxName, test_css_data),
"Should register placeholder with max length name");
// Test placeholder name over maximum length
char overName[30];
overName[0] = '%';
for (int i = 1; i < 27; i++) {
overName[i] = 'A';
}
overName[27] = '%';
overName[28] = '\0';
TEST_ASSERT_FALSE_MESSAGE(registry.registerProgmemData(overName, test_css_data),
"Should reject placeholder with over max length name");
// Test length calculation
size_t progmemLen = PlaceholderRegistry::getProgmemLength(test_css_data);
TEST_ASSERT_GREATER_THAN_MESSAGE(0, progmemLen, "Should calculate PROGMEM length");
// Test length calculation with null
size_t nullLen = PlaceholderRegistry::getProgmemLength(nullptr);
TEST_ASSERT_EQUAL_MESSAGE(0, nullLen, "Should return 0 for null PROGMEM data");
// Test RAM length calculation
size_t ramLen = PlaceholderRegistry::getRamLength((const void*)getTestRamData);
TEST_ASSERT_GREATER_THAN_MESSAGE(0, ramLen, "Should calculate RAM length");
// Test RAM length calculation with null
size_t nullRamLen = PlaceholderRegistry::getRamLength(nullptr);
TEST_ASSERT_EQUAL_MESSAGE(0, nullRamLen, "Should return 0 for null RAM getter");
Serial.println("[TEST] PlaceholderRegistry edge case tests completed successfully");
}
@@ -0,0 +1,324 @@
#include <unity.h>
#include <Arduino.h>
#include <TemplateEngine.h>
#include "../templates/simple_templates.h"
#include "../utils/test_utils.h"
// Test TemplateContext initialization
void test_template_context_initialization() {
Serial.println("[TEST] Testing TemplateContext initialization...");
TemplateContext ctx;
// Test default constructor
TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::TEXT, ctx.state,
"Initial state should be TEXT");
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.renderingDepth, "Initial renderingDepth should be 0");
TEST_ASSERT_NULL_MESSAGE(ctx.getCurrentContext(), "Initial context should be null");
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.placeholderPos, "Initial placeholderPos should be 0");
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferPos, "Initial bufferPos should be 0");
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferLen, "Initial bufferLen should be 0");
TEST_ASSERT_NULL_MESSAGE(ctx.registry, "Initial registry should be null");
// Test reset
ctx.pushContext(RenderingContextType::TEMPLATE, "TEST");
ctx.renderingDepth = 5;
ctx.reset();
TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::TEXT, ctx.state,
"Reset state should be TEXT");
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.renderingDepth, "Reset renderingDepth should be 0");
TEST_ASSERT_NULL_MESSAGE(ctx.getCurrentContext(), "Reset context should be null");
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.placeholderPos, "Reset placeholderPos should be 0");
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferPos, "Reset bufferPos should be 0");
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferLen, "Reset bufferLen should be 0");
// Test isComplete
TEST_ASSERT_FALSE_MESSAGE(ctx.isComplete(), "Should not be complete initially");
ctx.state = TemplateRenderState::COMPLETE;
TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "Should be complete when state is COMPLETE");
ctx.state = TemplateRenderState::ERROR;
TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "Should be complete when state is ERROR");
// Test hasError
ctx.state = TemplateRenderState::TEXT;
TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "Should not have error initially");
ctx.state = TemplateRenderState::ERROR;
TEST_ASSERT_TRUE_MESSAGE(ctx.hasError(), "Should have error when state is ERROR");
// Test getStateString
ctx.state = TemplateRenderState::TEXT;
TEST_ASSERT_EQUAL_STRING_MESSAGE("TEXT", ctx.getStateString().c_str(),
"Should return correct state string for TEXT");
ctx.state = TemplateRenderState::RENDERING_CONTEXT;
TEST_ASSERT_EQUAL_STRING_MESSAGE("RENDERING_CONTEXT", ctx.getStateString().c_str(),
"Should return correct state string for RENDERING_CONTEXT");
ctx.state = TemplateRenderState::COMPLETE;
TEST_ASSERT_EQUAL_STRING_MESSAGE("COMPLETE", ctx.getStateString().c_str(),
"Should return correct state string for COMPLETE");
ctx.state = TemplateRenderState::ERROR;
TEST_ASSERT_EQUAL_STRING_MESSAGE("ERROR", ctx.getStateString().c_str(),
"Should return correct state string for ERROR");
// Test setRegistry
PlaceholderRegistry registry(10);
ctx.setRegistry(&registry);
TEST_ASSERT_EQUAL_MESSAGE(&registry, ctx.registry, "Should set registry correctly");
Serial.println("[TEST] TemplateContext initialization tests completed successfully");
}
// Test TemplateContext stack
void test_template_context_stack() {
Serial.println("[TEST] Testing TemplateContext stack...");
TemplateContext ctx;
const char* template1 = plain_text_template;
const char* template2 = single_placeholder_template;
const char* template3 = multiple_placeholders_template;
size_t len1 = strlen_P(template1);
size_t len2 = strlen_P(template2);
size_t len3 = strlen_P(template3);
// Test push context
ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE1%");
RenderingContext* ctx1 = ctx.getCurrentContext();
ctx1->context.templateCtx.templateData = template1;
ctx1->context.templateCtx.templateLen = len1;
ctx1->context.templateCtx.isProgmem = true;
ctx1->context.templateCtx.position = 0;
TEST_ASSERT_EQUAL_MESSAGE(1, ctx.renderingDepth, "Stack depth should be 1 after push");
TEST_ASSERT_NOT_NULL_MESSAGE(ctx.getCurrentContext(), "Current context should not be null");
TEST_ASSERT_EQUAL_MESSAGE(template1, ctx.getCurrentContext()->context.templateCtx.templateData,
"Current template should be pushed template");
TEST_ASSERT_EQUAL_MESSAGE(len1, ctx.getCurrentContext()->context.templateCtx.templateLen, "Template length should be set");
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.getCurrentContext()->context.templateCtx.position, "Template position should be reset");
// Test push multiple templates
ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE2%");
RenderingContext* ctx2 = ctx.getCurrentContext();
ctx2->context.templateCtx.templateData = template2;
ctx2->context.templateCtx.templateLen = len2;
ctx2->context.templateCtx.isProgmem = true;
ctx2->context.templateCtx.position = 0;
TEST_ASSERT_EQUAL_MESSAGE(2, ctx.renderingDepth, "Stack depth should be 2 after second push");
TEST_ASSERT_EQUAL_MESSAGE(template2, ctx.getCurrentContext()->context.templateCtx.templateData,
"Current template should be second pushed template");
ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE3%");
RenderingContext* ctx3 = ctx.getCurrentContext();
ctx3->context.templateCtx.templateData = template3;
ctx3->context.templateCtx.templateLen = len3;
ctx3->context.templateCtx.isProgmem = true;
ctx3->context.templateCtx.position = 0;
TEST_ASSERT_EQUAL_MESSAGE(3, ctx.renderingDepth, "Stack depth should be 3 after third push");
// Test pop context
ctx.popContext();
TEST_ASSERT_EQUAL_MESSAGE(2, ctx.renderingDepth, "Stack depth should be 2 after pop");
TEST_ASSERT_EQUAL_MESSAGE(template2, ctx.getCurrentContext()->context.templateCtx.templateData,
"Current template should be previous template");
ctx.popContext();
TEST_ASSERT_EQUAL_MESSAGE(1, ctx.renderingDepth, "Stack depth should be 1 after second pop");
TEST_ASSERT_EQUAL_MESSAGE(template1, ctx.getCurrentContext()->context.templateCtx.templateData,
"Current template should be first template");
ctx.popContext();
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.renderingDepth, "Stack depth should be 0 after third pop");
// Test stack overflow
// Push up to MAX_RENDERING_DEPTH
for (int i = 0; i < TemplateContext::MAX_RENDERING_DEPTH; i++) {
ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE%");
RenderingContext* c = ctx.getCurrentContext();
c->context.templateCtx.templateData = template1;
c->context.templateCtx.templateLen = len1;
c->context.templateCtx.isProgmem = true;
c->context.templateCtx.position = 0;
}
TEST_ASSERT_EQUAL_MESSAGE(TemplateContext::MAX_RENDERING_DEPTH, ctx.renderingDepth,
"Stack depth should be at MAX_RENDERING_DEPTH");
// Try to push one more (should error)
ctx.pushContext(RenderingContextType::TEMPLATE, "%OVERFLOW%");
TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::ERROR, ctx.state,
"State should be ERROR after stack overflow");
// Test stack underflow
ctx.reset();
ctx.popContext();
TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::ERROR, ctx.state,
"State should be ERROR after stack underflow");
// Test getStackTrace
ctx.reset();
ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE1%");
ctx.getCurrentContext()->context.templateCtx.templateData = template1;
ctx.getCurrentContext()->context.templateCtx.templateLen = len1;
ctx.getCurrentContext()->context.templateCtx.isProgmem = true;
ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE2%");
ctx.getCurrentContext()->context.templateCtx.templateData = template2;
ctx.getCurrentContext()->context.templateCtx.templateLen = len2;
ctx.getCurrentContext()->context.templateCtx.isProgmem = true;
String trace = ctx.getStackTrace();
TEST_ASSERT_TRUE_MESSAGE(trace.length() > 0, "Stack trace should not be empty");
TEST_ASSERT_TRUE_MESSAGE(trace.indexOf("TEMPLATE1") >= 0,
"Stack trace should contain template1");
TEST_ASSERT_TRUE_MESSAGE(trace.indexOf("TEMPLATE2") >= 0,
"Stack trace should contain template2");
Serial.println("[TEST] TemplateContext stack tests completed successfully");
}
// Test TemplateContext buffer management
void test_template_context_buffer() {
Serial.println("[TEST] Testing TemplateContext buffer management...");
TemplateContext ctx;
const char* templateData = plain_text_template;
size_t templateLen = strlen_P(templateData);
// Push a template context first
ctx.pushContext(RenderingContextType::TEMPLATE, "TEST");
RenderingContext* currentCtx = ctx.getCurrentContext();
currentCtx->context.templateCtx.templateData = templateData;
currentCtx->context.templateCtx.templateLen = templateLen;
currentCtx->context.templateCtx.isProgmem = true;
currentCtx->context.templateCtx.position = 0;
// Test refillBuffer
bool refilled = ctx.refillBuffer();
TEST_ASSERT_TRUE_MESSAGE(refilled, "Should refill buffer successfully");
TEST_ASSERT_GREATER_THAN_MESSAGE(0, ctx.bufferLen, "Buffer length should be greater than 0");
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferPos, "Buffer position should be 0 after refill");
// Test getNextChar
char c1 = ctx.getNextChar();
TEST_ASSERT_EQUAL_MESSAGE('H', c1, "Should get first character");
TEST_ASSERT_EQUAL_MESSAGE(1, ctx.bufferPos, "Buffer position should be 1");
// Test getNextChar multiple times
char c2 = ctx.getNextChar();
char c3 = ctx.getNextChar();
TEST_ASSERT_EQUAL_MESSAGE('e', c2, "Should get second character");
TEST_ASSERT_EQUAL_MESSAGE('l', c3, "Should get third character");
// Test getAvailableBytes
size_t available = ctx.getAvailableBytes();
TEST_ASSERT_GREATER_THAN_MESSAGE(0, available, "Should have available bytes");
// Test hasMoreData
TEST_ASSERT_TRUE_MESSAGE(ctx.hasMoreData(), "Should have more data initially");
// Test getNextChar until end
ctx.reset();
ctx.pushContext(RenderingContextType::TEMPLATE, "TEST");
currentCtx = ctx.getCurrentContext();
currentCtx->context.templateCtx.templateData = templateData;
currentCtx->context.templateCtx.templateLen = templateLen;
currentCtx->context.templateCtx.isProgmem = true;
currentCtx->context.templateCtx.position = 0;
int charCount = 0;
while (ctx.hasMoreData()) {
char c = ctx.getNextChar();
if (c == '\0') {
break;
}
charCount++;
}
TEST_ASSERT_GREATER_THAN_MESSAGE(0, charCount, "Should read all characters");
// Test refillBuffer when at end
ctx.reset();
ctx.pushContext(RenderingContextType::TEMPLATE, "TEST");
currentCtx = ctx.getCurrentContext();
currentCtx->context.templateCtx.templateData = templateData;
currentCtx->context.templateCtx.templateLen = templateLen;
currentCtx->context.templateCtx.isProgmem = true;
currentCtx->context.templateCtx.position = templateLen;
bool refilled2 = ctx.refillBuffer();
TEST_ASSERT_FALSE_MESSAGE(refilled2, "Should not refill when at end");
// Test getNextChar when at end
ctx.reset();
ctx.pushContext(RenderingContextType::TEMPLATE, "TEST");
currentCtx = ctx.getCurrentContext();
currentCtx->context.templateCtx.templateData = templateData;
currentCtx->context.templateCtx.templateLen = templateLen;
currentCtx->context.templateCtx.position = templateLen;
char c4 = ctx.getNextChar();
TEST_ASSERT_EQUAL_MESSAGE('\0', c4, "Should return null character at end");
// Test resetPlaceholder
ctx.placeholderPos = 10;
strcpy(ctx.placeholderName, "%TEST%");
ctx.resetPlaceholder();
TEST_ASSERT_EQUAL_MESSAGE(0, ctx.placeholderPos, "Placeholder position should be reset");
TEST_ASSERT_EQUAL_MESSAGE(0, strlen(ctx.placeholderName),
"Placeholder name should be cleared");
Serial.println("[TEST] TemplateContext buffer tests completed successfully");
}
// Test TemplateContext state management
void test_template_context_state() {
Serial.println("[TEST] Testing TemplateContext state management...");
TemplateContext ctx;
// Test all state strings
ctx.state = TemplateRenderState::TEXT;
TEST_ASSERT_EQUAL_STRING_MESSAGE("TEXT", ctx.getStateString().c_str(),
"Should return TEXT for TEXT state");
ctx.state = TemplateRenderState::BUILDING_PLACEHOLDER;
TEST_ASSERT_EQUAL_STRING_MESSAGE("BUILDING_PLACEHOLDER", ctx.getStateString().c_str(),
"Should return BUILDING_PLACEHOLDER for BUILDING_PLACEHOLDER state");
ctx.state = TemplateRenderState::RENDERING_CONTEXT;
TEST_ASSERT_EQUAL_STRING_MESSAGE("RENDERING_CONTEXT", ctx.getStateString().c_str(),
"Should return RENDERING_CONTEXT for RENDERING_CONTEXT state");
ctx.state = TemplateRenderState::COMPLETE;
TEST_ASSERT_EQUAL_STRING_MESSAGE("COMPLETE", ctx.getStateString().c_str(),
"Should return COMPLETE for COMPLETE state");
ctx.state = TemplateRenderState::ERROR;
TEST_ASSERT_EQUAL_STRING_MESSAGE("ERROR", ctx.getStateString().c_str(),
"Should return ERROR for ERROR state");
// Test isComplete for all states
ctx.state = TemplateRenderState::TEXT;
TEST_ASSERT_FALSE_MESSAGE(ctx.isComplete(), "TEXT should not be complete");
ctx.state = TemplateRenderState::BUILDING_PLACEHOLDER;
TEST_ASSERT_FALSE_MESSAGE(ctx.isComplete(), "BUILDING_PLACEHOLDER should not be complete");
ctx.state = TemplateRenderState::RENDERING_CONTEXT;
TEST_ASSERT_FALSE_MESSAGE(ctx.isComplete(), "RENDERING_CONTEXT should not be complete");
ctx.state = TemplateRenderState::COMPLETE;
TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "COMPLETE should be complete");
ctx.state = TemplateRenderState::ERROR;
TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "ERROR should be complete");
// Test hasError for all states
ctx.state = TemplateRenderState::TEXT;
TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "TEXT should not have error");
ctx.state = TemplateRenderState::ERROR;
TEST_ASSERT_TRUE_MESSAGE(ctx.hasError(), "ERROR should have error");
Serial.println("[TEST] TemplateContext state tests completed successfully");
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
#include "test_utils.h"
#include <unity.h>
#include <pgmspace.h>
// Helper function to capture rendered output
String captureRenderedOutput(TemplateContext& ctx, size_t bufferSize) {
String output;
uint8_t* buffer = new uint8_t[bufferSize];
if (!buffer) {
return output;
}
while (!TemplateRenderer::isComplete(ctx)) {
size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, bufferSize);
if (written > 0) {
// Arduino String doesn't have (const char*, size_t) constructor
// Use substring approach or create temporary string
char* temp = new char[written + 1];
memcpy(temp, buffer, written);
temp[written] = '\0';
output += String(temp);
delete[] temp;
} else {
break;
}
}
delete[] buffer;
return output;
}
// Helper function to compare rendered output
bool compareRenderedOutput(const String& expected, TemplateContext& ctx, size_t bufferSize) {
String actual = captureRenderedOutput(ctx, bufferSize);
return actual == expected;
}
// Note: For test RAM getters, define them as regular functions in test files
// Example:
// static String testData = "test";
// const char* getTestData() { return testData.c_str(); }
// Helper function to verify template output
void verifyTemplateOutput(const char* templateData, PlaceholderRegistry& registry, const String& expected) {
TemplateContext ctx;
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, templateData);
String actual = captureRenderedOutput(ctx);
TEST_ASSERT_EQUAL_STRING_MESSAGE(expected.c_str(), actual.c_str(),
"Template output should match expected");
}
// Helper function to render template to string
String renderTemplateToString(const char* templateData, PlaceholderRegistry& registry, size_t bufferSize) {
TemplateContext ctx;
ctx.setRegistry(&registry);
TemplateRenderer::initializeContext(ctx, templateData);
return captureRenderedOutput(ctx, bufferSize);
}
@@ -0,0 +1,34 @@
#ifndef TEST_UTILS_H
#define TEST_UTILS_H
#include <Arduino.h>
#include <TemplateEngine.h>
// Test structure and state management
using TestFn = void(*)();
struct TestCase {
const char* name;
TestFn fn;
uint16_t line;
};
#define TEST_ENTRY(fn) { #fn, fn, __LINE__ }
// Helper function to capture rendered output
String captureRenderedOutput(TemplateContext& ctx, size_t bufferSize = 512);
// Helper function to compare rendered output
bool compareRenderedOutput(const String& expected, TemplateContext& ctx, size_t bufferSize = 512);
// Helper function to create test RAM getter
// Note: This is a simple wrapper - for actual tests, define getter functions directly
// Helper function to verify template output
void verifyTemplateOutput(const char* templateData, PlaceholderRegistry& registry, const String& expected);
// Helper function to render template to string
String renderTemplateToString(const char* templateData, PlaceholderRegistry& registry, size_t bufferSize = 512);
#endif // TEST_UTILS_H