agents/tests.md
For comprehensive guidelines, read these files:
| Topic | Read |
|---|---|
| Test commands and execution | agents/docs/testing-commands.md |
| Build system restrictions | agents/docs/build-system.md |
| C++ coding standards | agents/docs/cpp-standards.md |
| MCP server tools | agents/docs/mcp-tools.md |
| Debugging strategies | agents/docs/debugging.md |
| LLDB debugging guide | agents/docs/lldb-debugging.md |
This file contains only test-directory-specific conventions.
ALL BACKGROUND AGENTS MUST FOLLOW THESE REQUIREMENTS BEFORE INDICATING COMPLETION:
🚨 ALWAYS RUN bash test BEFORE INDICATING COMPLETION
bash test runs the full test suite including unit tests and compilation checks🚨 USE MCP SERVER VALIDATION TOOL
validate_completion tool from the MCP server: uv run mcp_server.pybash test and validates that all tests pass🚨 ZERO TOLERANCE FOR TEST FAILURES
bash test has been run and ALL tests passvalidate_completion tool shows successFAILURE TO FOLLOW THESE REQUIREMENTS WILL RESULT IN BROKEN CODE SUBMISSIONS.
The bash test mandate above does NOT apply when a sub-agent is dispatched as one step of a larger multi-step orchestration.
When the orchestrator (the parent agent) delegates a single step of a 3+ step plan to a sub-agent — e.g., "move file X, sweep includes, commit" as part of a 9-step refactor — that sub-agent MUST:
bash lint only. It is fast and catches missing-include errors.bash test --cpp / bash test. Per-step test runs across N sub-agents = N redundant compiles of the same codebase. The test pass should run ONCE.bash test --cpp once at the end before pushing. The Stop hook (ci/hooks/check-on-stop.py) also runs it automatically when files changed during the session, so the orchestrator gets it for free.How a sub-agent knows it is orchestrated: the dispatcher's prompt explicitly says so (e.g., "you are step N of an M-step plan; do not run bash test"). If the prompt does not say this, fall back to the standard mandate above and run bash test.
Orchestrator responsibilities:
bash test --cpp once after the final step's commit, before pushing the branch / opening the PR.git bisect or git log) and dispatch a fix.🚨 CRITICAL: Always use the proper assertion macros for better error messages and debugging:
CHECK_EQ(A, B) - for equality comparisonsCHECK(A == B) - provides poor error messagesCHECK_LT(A, B) - for less than comparisonsCHECK_LE(A, B) - for less than or equal comparisonsCHECK_GT(A, B) - for greater than comparisonsCHECK_GE(A, B) - for greater than or equal comparisonsCHECK(A < B), CHECK(A <= B), CHECK(A > B), CHECK(A >= B)CHECK_TRUE(condition) - for true conditionsCHECK_FALSE(condition) - for false conditionsCHECK(condition) - for boolean checksCHECK_STREQ(str1, str2) - for string equalityCHECK_STRNE(str1, str2) - for string inequalityCHECK(str1 == str2) - for string comparisonsCHECK_DOUBLE_EQ(a, b) - for floating point equalityCHECK_DOUBLE_NE(a, b) - for floating point inequalityCHECK(a == b) - for floating point comparisons// Good assertion usage
CHECK_EQ(expected_value, actual_value);
CHECK_LT(current_index, max_index);
CHECK_GT(temperature, 0.0);
CHECK_TRUE(is_initialized);
CHECK_FALSE(has_error);
CHECK_STREQ("expected", actual_string);
CHECK_DOUBLE_EQ(3.14159, pi_value, 0.001);
// Bad assertion usage
CHECK(expected_value == actual_value); // Poor error messages
CHECK(current_index < max_index); // Poor error messages
CHECK(is_initialized); // Unclear intent
CHECK("expected" == actual_string); // Wrong comparison type
Why: Using the proper assertion macros provides:
🚨 CRITICAL: Always use FL_ prefixed trampolines instead of direct doctest macros
All test files must use FL_CHECK and FL_REQUIRE macro trampolines instead of calling doctest macros directly. This provides a consistent abstraction layer between test code and the underlying test framework (doctest).
The FastLED test suite uses a trampoline layer defined in tests/test.h that wraps doctest macros:
// tests/test.h defines trampolines
#define FL_CHECK(...) CHECK(__VA_ARGS__)
#define FL_CHECK_EQ(a, b) CHECK_EQ(a, b)
#define FL_REQUIRE(...) REQUIRE(__VA_ARGS__)
// ... and 30+ more variants
✅ CORRECT - Use FL_ prefixed trampolines:
#include "test.h" // Includes trampolines, not doctest.h directly
TEST_CASE("Example test") {
FL_CHECK_EQ(expected, actual);
FL_REQUIRE_LT(index, max_size);
FL_CHECK_TRUE(condition);
FL_CHECK_FALSE(error_flag);
}
❌ INCORRECT - Do NOT use doctest macros directly:
#include "doctest.h" // Wrong - use test.h instead
TEST_CASE("Example test") {
CHECK_EQ(expected, actual); // Missing FL_ prefix
REQUIRE_LT(index, max_size); // Missing FL_ prefix
CHECK(condition == true); // Missing FL_ prefix
}
All 35+ trampolines follow the pattern: FL_<DOCTEST_MACRO_NAME>
Basic Assertions:
FL_CHECK(...) - Basic checkFL_REQUIRE(...) - Required check (stops test on failure)Equality Comparisons:
FL_CHECK_EQ(a, b) / FL_REQUIRE_EQ(a, b) - EqualFL_CHECK_NE(a, b) / FL_REQUIRE_NE(a, b) - Not equalRelational Comparisons:
FL_CHECK_LT(a, b) / FL_REQUIRE_LT(a, b) - Less thanFL_CHECK_LE(a, b) / FL_REQUIRE_LE(a, b) - Less than or equalFL_CHECK_GT(a, b) / FL_REQUIRE_GT(a, b) - Greater thanFL_CHECK_GE(a, b) / FL_REQUIRE_GE(a, b) - Greater than or equalBoolean Assertions:
FL_CHECK_TRUE(x) / FL_REQUIRE_TRUE(x) - Must be trueFL_CHECK_FALSE(x) / FL_REQUIRE_FALSE(x) - Must be falseFloating Point:
FL_CHECK_DOUBLE_EQ(a, b) / FL_REQUIRE_DOUBLE_EQ(a, b) - Double equalityFL_CHECK_DOUBLE_NE(a, b) / FL_REQUIRE_DOUBLE_NE(a, b) - Double inequalityString Comparisons:
FL_CHECK_STREQ(a, b) / FL_REQUIRE_STREQ(a, b) - String equalFL_CHECK_STRNE(a, b) / FL_REQUIRE_STRNE(a, b) - String not equalType Traits:
FL_CHECK_TRAIT(expr) / FL_REQUIRE_TRAIT(expr) - Type trait validationUnary Checks:
FL_CHECK_UNARY(expr) / FL_REQUIRE_UNARY(expr) - Unary expressionFL_CHECK_UNARY_FALSE(expr) / FL_REQUIRE_UNARY_FALSE(expr) - Unary falseThrowing Assertions:
FL_CHECK_THROWS(...) / FL_REQUIRE_THROWS(...) - Must throw any exceptionFL_CHECK_THROWS_AS(expr, type) / FL_REQUIRE_THROWS_AS(expr, type) - Must throw specific typeFL_CHECK_THROWS_WITH(expr, str) / FL_REQUIRE_THROWS_WITH(expr, str) - Must throw with messageFL_CHECK_THROWS_WITH_AS(expr, str, type) / FL_REQUIRE_THROWS_WITH_AS(expr, str, type) - CombinedFL_CHECK_NOTHROW(...) / FL_REQUIRE_NOTHROW(...) - Must not throwThese macros should NOT use FL_ prefix:
CHECK_CLOSE / REQUIRE_CLOSE - Custom floating point comparison macros (not doctest)TEST_CASE / SUBCASE / TEST_SUITE - Test structure macros (remain unchanged)DOCTEST_CONFIG_* - Configuration macros (remain unchanged)🚨 CRITICAL: Wrap template expressions with commas in parentheses
When passing template expressions containing commas to FL_ macros, the preprocessor treats commas as argument separators. Wrap the entire expression in parentheses:
❌ PROBLEM - Preprocessor error:
// ERROR: Preprocessor sees 3 arguments instead of 2
FL_CHECK_EQ(int_scale<T1, T2>(arg), expected)
// ^^^ comma treated as macro argument separator
✅ SOLUTION - Wrap in parentheses:
// Correct: Parentheses protect the comma from preprocessor
FL_CHECK_EQ((int_scale<T1, T2>(arg)), expected)
// ^ ^
// wrap template expression
More examples:
// Multiple template arguments
FL_CHECK_TRUE((std::is_same<A, B>::value))
// Template function calls
FL_CHECK_EQ((convert<uint8_t, uint16_t>(input)), output)
// Nested templates
FL_REQUIRE_LT((map_range<int, int, long>(x, 0, 100, 0, 1000)), max_value)
When wrapping is needed:
func<T1, T2>(...)std::is_same<A, B>::valueContainer<K, V>::size()When wrapping is NOT needed:
FL_CHECK_EQ(func(a, b, c), expected) ✅ (commas are function args)FL_CHECK_EQ(vec, {1, 2, 3}) ✅ (braces protect commas)🚨 CRITICAL: Minimize test file proliferation - Consolidate tests whenever possible
TEST_CASE() blocks with additional scenariosSome test directories use a consolidation pattern where multiple .cpp files in a subdirectory are #included into a single parent .cpp file that compiles as one test binary. These subdirectories are listed in EXCLUDED_TEST_DIRS in tests/test_config.py so the auto-discovery system doesn't try to compile them individually.
Example: tests/fl/fx/2d/ contains individual test files (animartrix_fp.cpp, chasing_spirals.cpp, etc.) that are all #included by the parent file tests/fl/fx/2d.cpp:
// tests/fl/fx/2d.cpp
// ok standalone // ok cpp include
#include "2d/animartrix_fp.cpp"
#include "2d/chasing_spirals.cpp"
#include "2d/perlin_s8x8_test.cpp"
Key points:
// ok standalone comment at the top of these files is a linter exemption (tells lint checks the file is valid despite being #included rather than compiled on its own). It is not a build system registration mechanism..cpp file's #include listEXCLUDED_TEST_DIRS in tests/test_config.py):
tests/fl/fx/2d/ → parent: tests/fl/fx/2d.cpptests/fl/chipsets/encoders/tests/fl/log/tests/fl/audio/detectors/tests/fl/channels/detail/validation/tests/fl/remote/rpc/tests/fl/codec/🚨 NEVER create tests in tests/misc/ - this is a legacy catch-all that should not grow.
When creating a new test file, mirror the source directory structure:
src/fl/stl/flat_map.h → test at tests/fl/stl/flat_map.cppsrc/platforms/esp/32/drivers/uart/wave8_encoder_uart.h → test at tests/platforms/esp/32/drivers/uart/wave8_encoder_uart.cppBefore creating a new file, always check:
tests/misc/ - mirror source directory structure insteadTEST_CASE in the same file?Why: Maintaining a clean, consolidated test suite:
🚨 CRITICAL: KEEP TESTS AS SIMPLE AS POSSIBLE
When writing or updating tests in tests/*, prioritize absolute simplicity over comprehensive coverage:
❌ BAD - Over-engineered with mocks and helpers:
class MockTime {
static uint32_t value;
static void set(uint32_t v) { value = v; }
static void advance(uint32_t d) { value += d; }
static uint32_t get() { return value; }
};
TEST_CASE("Timeout - comprehensive") {
SUBCASE("basic") { /* ... */ }
SUBCASE("rollover case 1") { /* ... */ }
SUBCASE("rollover case 2") { /* ... */ }
SUBCASE("edge case 1") { /* ... */ }
// ... 20 more subcases
}
✅ GOOD - Simple, direct, focused:
TEST_CASE("Timeout - rollover test") {
// Test critical rollover: starts before 0xFFFFFFFF, ends after 0x00000000
uint32_t start = 0xFFFFFF00;
Timeout timeout(start, 512);
CHECK_FALSE(timeout.done(start));
CHECK(timeout.done(start + 512)); // Works across rollover
}
Why the good example is better:
When updating existing tests that violate these principles:
Remember: A simple test that catches real bugs is infinitely more valuable than a complex test suite that's hard to maintain.
✅ CORRECT blank test file structure:
// Unit tests for general allocator functionality and integration tests
#include "test.h"
#include "FastLED.h"
using namespace fl;
TEST_CASE("New test - fill in") {
}
❌ WRONG patterns to avoid:
test.h, FastLED.h)using namespace fl; declaration🚨 CRITICAL: Anonymous namespaces in test files should match the test name:
For test files in tests/**/*, use an anonymous namespace named after the test:
// File: tests/fl/chipsets/test_ucs7604.cpp
#include "test.h"
#include "FastLED.h"
using namespace fl;
namespace { // Anonymous namespace for test_ucs7604
// Helper functions and test fixtures specific to this test
class MockController { /*...*/ };
void helperFunction() { /*...*/ }
TEST_CASE("UCS7604 - feature test") {
// Test implementation
}
} // anonymous namespace
Why:
Rules:
namespace { ... } for all test helpers and fixtures} // anonymous namespaceusing namespace fl; before the anonymous namespacenamespace test_ucs7604 { ... })🚨 ALL AGENTS: Read agents/docs/testing-commands.md and relevant agents/docs/*.md files before concluding testing work to refresh memory about test execution requirements and validation rules.