first commit

This commit is contained in:
stuce-bot 2025-06-30 20:47:33 +02:00
commit 5893b00dd2
1669 changed files with 1982740 additions and 0 deletions

View file

@ -0,0 +1,278 @@
# Note that we are using the zig compiler as a drop-in replacement for
# gcc. This allows the unit tests to be compiled across different platforms
# without having to worry about the underlying compiler.
cmake_minimum_required(VERSION 3.10)
project(FastLED_Tests)
# Enforce C++17 globally for all targets.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Force creation of thin archives (instead of full .a files) only for non apple builds
if(NOT APPLE)
set(CMAKE_CXX_ARCHIVE_CREATE "<CMAKE_AR> rcT <TARGET> <OBJECTS>")
set(CMAKE_CXX_ARCHIVE_APPEND "<CMAKE_AR> rT <TARGET> <OBJECTS>")
set(CMAKE_CXX_ARCHIVE_FINISH "<CMAKE_RANLIB> <TARGET>")
message(STATUS "Using thin archives for Clang build")
endif()
# Enable parallel compilation
include(ProcessorCount)
ProcessorCount(CPU_COUNT)
if(CPU_COUNT)
set(CMAKE_BUILD_PARALLEL_LEVEL ${CPU_COUNT})
endif()
# Check if mold linker is available
find_program(MOLD_EXECUTABLE mold)
if(MOLD_EXECUTABLE)
# Set mold as the default linker
message(STATUS "Using mold linker: ${MOLD_EXECUTABLE}")
# Add mold linker flags to the common flags
list(APPEND COMMON_COMPILE_FLAGS "-fuse-ld=mold")
# Set linker flags globally
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=mold")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=mold")
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -fuse-ld=mold")
else()
find_program(LLDLINK_EXECUTABLE lld-link)
if(LLDLINK_EXECUTABLE)
message(STATUS "Using lld-link linker: ${LLDLINK_EXECUTABLE}")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld")
else()
message(STATUS "Neither mold nor lld-link found. Using system default linker.")
endif()
endif()
# Set build type to Debug
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Choose the type of build." FORCE)
# Output the current build type
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
# Define common compiler flags and definitions
set(COMMON_COMPILE_FLAGS
-Wall
-Wextra
#-Wpedantic
-funwind-tables
-g3
-ggdb
-fno-omit-frame-pointer
-O0
-fno-inline
-Werror=return-type
-Werror=missing-declarations
-Werror=redundant-decls
-Werror=init-self
-Werror=missing-field-initializers
-Werror=pointer-arith
-Werror=write-strings
-Werror=format=2
-Werror=implicit-fallthrough
-Werror=missing-include-dirs
-Werror=date-time
-Werror=unused-parameter
-Werror=unused-variable
-Werror=unused-value
-Werror=cast-align
-DFASTLED_FIVE_BIT_HD_GAMMA_FUNCTION_2_8
-Wno-comment
# ignore Arduino/PlatformIO-specific PROGMEM macro
-DPROGMEM=
)
# C++-specific compiler flags
set(CXX_SPECIFIC_FLAGS
-Werror=suggest-override
-Werror=non-virtual-dtor
-Werror=reorder
-Werror=sign-compare
-Werror=float-equal
-Werror=mismatched-tags
-Werror=switch-enum
)
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND CXX_SPECIFIC_FLAGS -Werror=self-assign -Werror=infinite-recursion -Werror=extra-tokens -Werror=unused-private-field -Wglobal-constructors -Werror=global-constructors)
endif()
set(UNIT_TEST_COMPILE_FLAGS
-Wall
#-Wextra
#-Wpedantic
-funwind-tables
-g3
-ggdb
-fno-omit-frame-pointer
-O0
-Werror=return-type
-Werror=missing-declarations
#-Werror=redundant-decls
-Werror=init-self
#-Werror=missing-field-initializers
#-Werror=pointer-arith
#-Werror=write-strings
#-Werror=format=2
#-Werror=implicit-fallthrough
#-Werror=missing-include-dirs
-Werror=date-time
#-Werror=unused-parameter
#-Werror=unused-variable
#-Werror=unused-value
# Not supported in gcc.
#-Werror=infinite-recursion
#-v
-Wno-comment
)
set(UNIT_TEST_CXX_FLAGS
-Werror=suggest-override
-Werror=non-virtual-dtor
-Werror=switch-enum
#-Werror=reorder
#-Werror=sign-compare
#-Werror=float-equal
#-Werror=conversion
)
set(COMMON_COMPILE_DEFINITIONS
DEBUG
FASTLED_FORCE_NAMESPACE=1
FASTLED_NO_AUTO_NAMESPACE
FASTLED_TESTING
ENABLE_CRASH_HANDLER
FASTLED_STUB_IMPL
FASTLED_NO_PINMAP
HAS_HARDWARE_PIN_SUPPORT
_GLIBCXX_DEBUG
_GLIBCXX_DEBUG_PEDANTIC
)
# Set output directories
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/.build/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/.build/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/.build/bin)
# Set binary directory
set(CMAKE_BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/.build/bin)
# Set path to FastLED source directory
add_compile_definitions(${COMMON_COMPILE_DEFINITIONS})
set(FASTLED_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/..)
# Include FastLED source directory
include_directories(${FASTLED_SOURCE_DIR}/src)
# Delegate source file computation to src/CMakeLists.txt
add_subdirectory(${FASTLED_SOURCE_DIR}/src ${CMAKE_BINARY_DIR}/fastled)
if(NOT APPLE)
target_link_options(fastled PRIVATE -static-libgcc -static-libstdc++)
endif()
# Try to find libunwind, but make it optional
find_package(LibUnwind QUIET)
# Define a variable to check if we should use libunwind
set(USE_LIBUNWIND ${LibUnwind_FOUND})
if(USE_LIBUNWIND)
message(STATUS "LibUnwind found. Using it for better stack traces.")
else()
message(STATUS "LibUnwind not found. Falling back to basic stack traces.")
endif()
# Enable testing
enable_testing()
# Create doctest main library
add_library(doctest_main STATIC doctest_main.cpp)
target_include_directories(doctest_main PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(doctest_main PRIVATE ${UNIT_TEST_COMPILE_FLAGS})
target_compile_options(doctest_main PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${UNIT_TEST_CXX_FLAGS}>)
target_compile_definitions(doctest_main PRIVATE ${COMMON_COMPILE_DEFINITIONS})
# Find all test source files
file(GLOB TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/test_*.cpp")
# Find test executables (only actual test executables, not libraries)
file(GLOB TEST_BINARIES "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/test_*${CMAKE_EXECUTABLE_SUFFIX}")
# Process source files
foreach(TEST_SOURCE ${TEST_SOURCES})
get_filename_component(TEST_NAME ${TEST_SOURCE} NAME_WE)
add_executable(${TEST_NAME} ${TEST_SOURCE})
target_link_libraries(${TEST_NAME} fastled doctest_main)
# Set the correct subsystem for Windows
if(WIN32)
if(MSVC)
set_target_properties(${TEST_NAME} PROPERTIES
WIN32_EXECUTABLE FALSE
LINK_FLAGS "/SUBSYSTEM:CONSOLE")
else()
# For MinGW/Clang on Windows
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set_target_properties(${TEST_NAME} PROPERTIES
WIN32_EXECUTABLE FALSE
LINK_FLAGS "-Xlinker /subsystem:console")
else()
set_target_properties(${TEST_NAME} PROPERTIES
WIN32_EXECUTABLE FALSE)
endif()
endif()
endif()
# Add C++-specific flags only for C++ files
target_compile_options(fastled PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${CXX_SPECIFIC_FLAGS}>)
if(USE_LIBUNWIND)
target_link_libraries(${TEST_NAME} ${LIBUNWIND_LIBRARIES})
endif()
target_include_directories(${TEST_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
set_target_properties(${TEST_NAME} PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)
# Add static linking flags and debug flags for test executables
if(NOT APPLE AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_link_options(${TEST_NAME} PRIVATE -static-libgcc -static-libstdc++)
endif()
target_compile_options(${TEST_NAME} PRIVATE ${UNIT_TEST_COMPILE_FLAGS})
# Add C++-specific flags only for C++ files
target_compile_options(${TEST_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${UNIT_TEST_CXX_FLAGS}>)
# Add C++-specific flags only for C++ files
target_compile_options(${TEST_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${UNIT_TEST_CXX_FLAGS}>)
target_compile_definitions(${TEST_NAME} PRIVATE
${COMMON_COMPILE_DEFINITIONS}
$<$<BOOL:${USE_LIBUNWIND}>:USE_LIBUNWIND>
)
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
endforeach()
# Process remaining binaries (those without corresponding source files)
option(CLEAN_ORPHANED_BINARIES "Remove orphaned test binaries" ON)
if(CLEAN_ORPHANED_BINARIES)
foreach(ORPHANED_BINARY ${TEST_BINARIES})
get_filename_component(BINARY_NAME ${ORPHANED_BINARY} NAME_WE)
get_filename_component(BINARY_DIR ${ORPHANED_BINARY} DIRECTORY)
get_filename_component(PARENT_DIR ${BINARY_DIR} DIRECTORY)
get_filename_component(GRANDPARENT_DIR ${PARENT_DIR} DIRECTORY)
set(CORRESPONDING_SOURCE "${GRANDPARENT_DIR}/${BINARY_NAME}.cpp")
if(NOT EXISTS "${CORRESPONDING_SOURCE}")
message(STATUS "Found orphaned binary without source: ${ORPHANED_BINARY}")
file(REMOVE "${ORPHANED_BINARY}")
message(STATUS "Deleted orphaned binary: ${ORPHANED_BINARY}")
endif()
endforeach()
endif()
# Add verbose output for tests
set(CMAKE_CTEST_ARGUMENTS "--output-on-failure")

View file

@ -0,0 +1,49 @@
#ifndef CRASH_HANDLER_H
#define CRASH_HANDLER_H
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <execinfo.h>
#include <unistd.h>
#include <libunwind.h>
void print_stacktrace() {
unw_cursor_t cursor;
unw_context_t context;
unw_getcontext(&context);
unw_init_local(&cursor, &context);
int depth = 0;
while (unw_step(&cursor) > 0) {
unw_word_t offset, pc;
char sym[256];
unw_get_reg(&cursor, UNW_REG_IP, &pc);
if (pc == 0) {
break;
}
printf("#%-2d 0x%lx:", depth++, pc);
if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
printf(" (%s+0x%lx)\n", sym, offset);
} else {
printf(" -- symbol not found\n");
}
}
}
void crash_handler(int sig) {
fprintf(stderr, "Error: signal %d:\n", sig);
print_stacktrace();
exit(1);
}
void setup_crash_handler() {
signal(SIGSEGV, crash_handler);
signal(SIGABRT, crash_handler);
}
#endif // CRASH_HANDLER_H

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,25 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
// This file contains the main function for doctest
// It will be compiled once and linked to all test executables
#ifdef _WIN32
// Windows-specific entry point
#include <windows.h>
// Define the main entry point for Windows
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, INT nCmdShow) {
return doctest::Context().run();
}
// Also provide a standard main for compatibility
int main(int argc, char** argv) {
return doctest::Context(argc, argv).run();
}
#else
// Standard entry point for non-Windows platforms
int main(int argc, char** argv) {
return doctest::Context(argc, argv).run();
}
#endif

View file

@ -0,0 +1,3 @@
To run tests use
`uv run ci/cpp_test_run.py`

View file

@ -0,0 +1,91 @@
#pragma once
#include "doctest.h"
#include <stdint.h>
#include <vector>
#include <set>
#include <string>
#include <iostream>
#include "crgb.h"
#include "fl/str.h"
#include "fl/lut.h"
#include "fl/xypath.h"
#include "fl/tile2x2.h"
#include "fl/strstream.h"
#include "fl/hash_set.h"
#include "fl/vector.h"
using namespace fl;
namespace doctest {
template<> struct StringMaker<CRGB> {
static String convert(const CRGB& value) {
fl::Str out = value.toString();
return out.c_str();
}
};
template<> struct StringMaker<Str> {
static String convert(const Str& value) {
return value.c_str();
}
};
template<typename T> struct StringMaker<vec2<T>> {
static String convert(const vec2<T>& value) {
fl::Str out;
out += "vec2(";
out += value.x;
out += ", ";
out += value.y;
out += ")";
return out.c_str();
}
};
template<> struct StringMaker<Tile2x2_u8> {
static String convert(const Tile2x2_u8& value) {
fl::StrStream out;
out << "Tile2x2_u8(" << value.origin() << ")";
return out.c_str();
}
};
template<typename T> struct StringMaker<rect<T>> {
static String convert(const rect<T>& value) {
fl::Str out;
out += "rect(";
out += " (";
out += value.mMin.x;
out += ",";
out += value.mMin.y;
out += "), (";
out += value.mMax.x;
out += ",";
out += value.mMax.y;
out += "))";
return out.c_str();
}
};
template<typename Key, typename Hash, typename KeyEqual> struct StringMaker<fl::hash_set<Key, Hash, KeyEqual>> {
static String convert(const fl::hash_set<Key, Hash, KeyEqual>& value) {
fl::Str out;
out.append(value);
return out.c_str();
}
};
template<typename T>
struct StringMaker<fl::vector<T>> {
static String convert(const fl::vector<T>& value) {
fl::Str out;
out.append(value);
return out.c_str();
}
};
}

View file

@ -0,0 +1,27 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "fl/algorithm.h"
#include "fl/dbg.h"
#include "fl/vector.h"
#include "test.h"
using namespace fl;
TEST_CASE("reverse an int list") {
fl::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);
fl::reverse(vec.begin(), vec.end());
CHECK_EQ(vec[0], 5);
CHECK_EQ(vec[1], 4);
CHECK_EQ(vec[2], 3);
CHECK_EQ(vec[3], 2);
CHECK_EQ(vec[4], 1);
}

View file

@ -0,0 +1,283 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "FastLED.h"
#include "fl/five_bit_hd_gamma.h"
#include "assert.h"
#include "math.h"
#include <ctime>
#include <cstdlib>
#include <vector>
#include <algorithm>
#include <iostream>
#include <sstream>
#include "crgb.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
TEST_CASE("five_bit_bitshift") {
const uint16_t test_data[][2][4] = {
{ // test case
//r g b brightness
{0, 0, 0, 0}, // input
{0, 0, 0, 0}, // output
},
{ // 0 brightness brings all colors down to 0
{0xffff, 0xffff, 0xffff, 0},
{0, 0, 0, 0},
},
{ // color values below 8 become 0 at max brightness
{8, 7, 0, 255},
{1, 0, 0, 1},
},
{
{0xffff, 0x00f0, 0x000f, 0x01},
{0x11, 0x00, 0x00, 0x01},
},
{
{0x0100, 0x00f0, 0x000f, 0xff},
{0x08, 0x08, 0x00, 0x03},
},
{
{0x2000, 0x1000, 0x0f00, 0x20},
{0x20, 0x10, 0x0f, 0x03},
},
{
{0xffff, 0x8000, 0x4000, 0x40},
{0x81, 0x41, 0x20, 0x0f},
},
{
{0xffff, 0x8000, 0x4000, 0x80},
{0x81, 0x41, 0x20, 0x1f},
},
{
{0xffff, 0xffff, 0xffff, 0xff},
{0xff, 0xff, 0xff, 0x1f},
},
};
for (const auto& data : test_data) {
CRGB out_color;
uint8_t out_brightness;
five_bit_bitshift(data[0][0], data[0][1], data[0][2], data[0][3], &out_color, &out_brightness);
INFO("input red ", data[0][0], " green ", data[0][1], " blue ", data[0][2], " brightness ", data[0][3]);
INFO("output red ", out_color.r, " green ", out_color.g, " blue ", out_color.b, " brightness ", out_brightness);
CHECK_EQ(out_color.r, data[1][0]);
CHECK_EQ(out_color.g, data[1][1]);
CHECK_EQ(out_color.b, data[1][2]);
CHECK_EQ(out_brightness, data[1][3]);
}
}
TEST_CASE("__builtin_five_bit_hd_gamma_bitshift") {
// NOTE: FASTLED_FIVE_BIT_HD_GAMMA_FUNCTION_2_8 is defined for this test in CMakeLists.txt
const uint8_t test_data[][2][4] = {
{ // test case
//r g b brightness
{0, 0, 0, 0}, // input
{0, 0, 0, 0}, // output
},
{ // 0 brightness brings all colors down to 0
{255, 255, 255, 0},
{0, 0, 0, 0},
},
{
{16, 16, 16, 16},
{0, 0, 0, 1},
},
{
{64, 64, 64, 8},
{4, 4, 4, 1},
},
{
{255, 127, 43, 1},
{17, 3, 0, 1},
},
{
{255, 127, 43, 1},
{17, 3, 0, 1},
},
{
{255, 127, 43, 64},
{129, 21, 1, 15},
},
{
{255, 127, 43, 255},
{255, 42, 3, 31},
},
{
{255, 255, 255, 255},
{255, 255, 255, 31},
},
};
for (const auto& data : test_data) {
CRGB out_color;
uint8_t out_brightness;
__builtin_five_bit_hd_gamma_bitshift(CRGB(data[0][0], data[0][1], data[0][2]), CRGB(255, 255, 255), data[0][3], &out_color, &out_brightness);
INFO("input red ", data[0][0], " green ", data[0][1], " blue ", data[0][2], " brightness ", data[0][3]);
INFO("output red ", out_color.r, " green ", out_color.g, " blue ", out_color.b, " brightness ", out_brightness);
CHECK_EQ(out_color.r, data[1][0]);
CHECK_EQ(out_color.g, data[1][1]);
CHECK_EQ(out_color.b, data[1][2]);
CHECK_EQ(out_brightness, data[1][3]);
}
}
#define CHECK_NEAR(a, b, c) CHECK_LT(abs(a - b), c)
#define STRESS_TEST 1
#define PROBLEMATIC_TEST 0
// Testing allows upto 21% error between power output of WS2812 and APA102 in HD mode.
// This is mostly due to the rounding errors for WS2812 when one of the channels is small
// and the rest are fairly large. One component will get rounded down to 0, while in
// apa mode it will bitshift to relevance.
const static float TOLERANCE = 0.21;
const static int NUM_TESTS = 10000;
const static size_t MAX_FAILURES = 30;
const static int CUTOFF = 11; // WS2812 will give bad results when one of the components is less than 10.
struct Power {
float power;
float power_5bit;
uint8_t power_5bit_u8;
};
static float power_diff(Power power) {
return abs(power.power - power.power_5bit);
}
static float power_rgb(CRGB color, uint8_t brightness) {
color *= brightness;
float out = color.r / 255.f + color.g / 255.f + color.b / 255.f;
return out / 3.0f;
}
static float compute_power_5bit(CRGB color, uint8_t power_5bit, uint8_t brightness) {
assert(power_5bit <= 31);
float rgb_pow = power_rgb(color, brightness);
float brightness_pow = (power_5bit) / 31.0f;
float out = rgb_pow * brightness_pow;
return out;
}
static float compute_power_apa102(CRGB color, uint8_t brightness, uint8_t* power_5bit) {
uint16_t r16 = map8_to_16(color.r);
uint16_t g16 = map8_to_16(color.g);
uint16_t b16 = map8_to_16(color.b);
CRGB out_colors;
uint8_t v5 = 31;
uint8_t post_brightness_scale = five_bit_bitshift(r16, g16, b16, brightness, &out_colors, power_5bit);
float power = compute_power_5bit(out_colors, v5, post_brightness_scale);
return power;
}
static float compute_power_ws2812(CRGB color, uint8_t brightness) {
float power = power_rgb(color, brightness);
return power;
}
static Power compute_power(uint8_t brightness8, CRGB color) {
uint8_t power_5bit_u8;
float power_5bit = compute_power_apa102(color, brightness8, &power_5bit_u8);
float power_rgb = compute_power_ws2812(color, brightness8);
return {power_rgb, power_5bit, power_5bit_u8};
}
static void make_random(CRGB* color, uint8_t* brightness) {
color->r = rand() % 256;
color->g = rand() % 256;
color->b = rand() % 256;
*brightness = rand() % 256;
}
struct Data {
CRGB color;
uint8_t brightness;
};
TEST_CASE("five_bit_hd_gamma_bitshift functionality") {
SUBCASE("Sanity test for defines") {
CHECK_EQ(FASTLED_HD_COLOR_MIXING, 1);
}
#if PROBLEMATIC_TEST
SUBCASE("problematic test2") {
// Failure, diff is 0.580777 brightness: 249 color: R: 103 G: 125 B: 236 power: 0 power_5bit: 31
CRGB color = {103, 125, 236};
uint8_t brightness = 249;
problematic_test(color, brightness);
FAIL("Problematic test failed");
}
#endif
#if STRESS_TEST
SUBCASE("Randomized Power Matching Test for 5 bit power") {
srand(0); // Seed the random number generator so we get consitent results.
std::vector<Data> failures;
for (int i = 0; i < NUM_TESTS; i++) {
CRGB color;
uint8_t brightness;
make_random(&color, &brightness);
// if one ore more of the compoents is less than 10 then skip.
if (color.r < CUTOFF || color.g < CUTOFF || color.b < CUTOFF || brightness < CUTOFF) {
// WS2812 does badly at this.
continue;
}
Power result = compute_power(brightness, color);
float diff = power_diff(result);
if (diff > TOLERANCE) {
failures.push_back({color, brightness});
while (failures.size() > MAX_FAILURES) {
// failures.pop_back();
// select smallest power difference and remove it.
auto it = std::min_element(failures.begin(), failures.end(), [](const Data& a, const Data& b) {
Power p1 = compute_power(a.brightness, a.color);
Power p2 = compute_power(b.brightness, b.color);
return power_diff(p1) < power_diff(p2);
});
failures.erase(it);
}
}
}
if (failures.size()) {
// sort by the power difference
std::sort(failures.begin(), failures.end(), [](const Data& a, const Data& b) {
Power p1 = compute_power(a.brightness, a.color);
Power p2 = compute_power(b.brightness, b.color);
return abs(p1.power - p1.power_5bit) > abs(p2.power - p2.power_5bit);
});
std::cout << "Failures:" << std::endl;
for (auto& failure : failures) {
Power p = compute_power(failure.brightness, failure.color);
std::string color_str = "R: " + std::to_string(failure.color.r) + " G: " + std::to_string(failure.color.g) + " B: " + std::to_string(failure.color.b);
std::cout << "Failure, diff is " << power_diff(p) << " brightness: " << int(failure.brightness) << " color: " << color_str << " power: " << p.power << " power_5bit: " << int(p.power_5bit_u8) << std::endl;
}
// FAIL("Failures found");
// make a oostream object
std::ostringstream oss;
oss << __FILE__ << ":" << __LINE__ << " Failures found";
FAIL(oss.str());
}
}
#endif
}

View file

@ -0,0 +1,270 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "fl/bitset.h"
#include "fl/bitset_dynamic.h"
using namespace fl;
TEST_CASE("test bitset") {
// defaultconstructed bitset is empty
bitset_fixed<10> bs;
REQUIRE_EQ(bs.none(), true);
REQUIRE_EQ(bs.count(), 0);
REQUIRE_EQ(bs.size(), 10);
// set a bit
bs.set(3);
REQUIRE_EQ(bs.test(3), true);
REQUIRE_EQ(bs[3], true);
REQUIRE_EQ(bs.any(), true);
REQUIRE_EQ(bs.count(), 1);
// reset that bit
bs.reset(3);
REQUIRE_EQ(bs.test(3), false);
REQUIRE_EQ(bs.none(), true);
// toggle a bit
bs.flip(2);
REQUIRE_EQ(bs.test(2), true);
bs.flip(2);
REQUIRE_EQ(bs.test(2), false);
// flip all bits
bitset_fixed<5> bs2;
for (size_t i = 0; i < 5; ++i)
bs2.set(i, (i % 2) == 0);
auto bs2_flipped = ~bs2;
for (size_t i = 0; i < 5; ++i)
REQUIRE_EQ(bs2_flipped.test(i), !bs2.test(i));
// all() and count()
bitset_fixed<4> bs3;
for (size_t i = 0; i < 4; ++i)
bs3.set(i);
REQUIRE_EQ(bs3.all(), true);
REQUIRE_EQ(bs3.count(), 4);
// check that the count can auto expand
bs3.set(100);
REQUIRE_EQ(bs3.count(), 4);
// bitwise AND, OR, XOR
bitset_fixed<4> a, b;
a.set(0); a.set(2);
b.set(1); b.set(2);
auto or_ab = a | b;
REQUIRE_EQ(or_ab.test(0), true);
REQUIRE_EQ(or_ab.test(1), true);
REQUIRE_EQ(or_ab.test(2), true);
REQUIRE_EQ(or_ab.test(3), false);
auto and_ab = a & b;
REQUIRE_EQ(and_ab.test(2), true);
REQUIRE_EQ(and_ab.test(0), false);
auto xor_ab = a ^ b;
REQUIRE_EQ(xor_ab.test(0), true);
REQUIRE_EQ(xor_ab.test(1), true);
REQUIRE_EQ(xor_ab.test(2), false);
// reset and none()
a.reset(); b.reset();
REQUIRE_EQ(a.none(), true);
// Test expected size of bitset_fixed
REQUIRE_EQ(bitset_fixed<8>().size(), 8);
REQUIRE_EQ(bitset_fixed<16>().size(), 16);
REQUIRE_EQ(bitset_fixed<32>().size(), 32);
REQUIRE_EQ(bitset_fixed<64>().size(), 64);
REQUIRE_EQ(bitset_fixed<100>().size(), 100);
REQUIRE_EQ(bitset_fixed<1000>().size(), 1000);
// Test memory size of bitset_fixed class (sizeof)
// For bitset_fixed<8>, we expect 1 uint64_t block (8 bytes)
REQUIRE_EQ(sizeof(bitset_fixed<8>), 8);
// For bitset_fixed<64>, we expect 1 uint64_t block (8 bytes)
REQUIRE_EQ(sizeof(bitset_fixed<64>), 8);
// For bitset_fixed<65>, we expect 2 uint64_t blocks (16 bytes)
REQUIRE_EQ(sizeof(bitset_fixed<65>), 16);
// For bitset_fixed<128>, we expect 2 uint64_t blocks (16 bytes)
REQUIRE_EQ(sizeof(bitset_fixed<128>), 16);
// For bitset_fixed<129>, we expect 3 uint64_t blocks (24 bytes)
REQUIRE_EQ(sizeof(bitset_fixed<129>), 24);
}
TEST_CASE("compare fixed and dynamic bitsets") {
// Test that fixed and dynamic bitsets behave the same
bitset_fixed<10> fixed_bs;
fl::bitset_dynamic dynamic_bs(10);
// Set the same bits in both
fixed_bs.set(1);
fixed_bs.set(5);
fixed_bs.set(9);
dynamic_bs.set(1);
dynamic_bs.set(5);
dynamic_bs.set(9);
// Verify they have the same state
REQUIRE_EQ(fixed_bs.size(), dynamic_bs.size());
REQUIRE_EQ(fixed_bs.count(), dynamic_bs.count());
for (size_t i = 0; i < 10; ++i) {
REQUIRE_EQ(fixed_bs.test(i), dynamic_bs.test(i));
}
}
TEST_CASE("test bitset_dynamic") {
// default-constructed bitset is empty
bitset_dynamic bs;
REQUIRE_EQ(bs.size(), 0);
REQUIRE_EQ(bs.none(), true);
REQUIRE_EQ(bs.count(), 0);
// resize and test
bs.resize(10);
REQUIRE_EQ(bs.size(), 10);
REQUIRE_EQ(bs.none(), true);
// set a bit
bs.set(3);
REQUIRE_EQ(bs.test(3), true);
REQUIRE_EQ(bs[3], true);
REQUIRE_EQ(bs.any(), true);
REQUIRE_EQ(bs.count(), 1);
// reset that bit
bs.reset(3);
REQUIRE_EQ(bs.test(3), false);
REQUIRE_EQ(bs.none(), true);
// toggle a bit
bs.flip(2);
REQUIRE_EQ(bs.test(2), true);
bs.flip(2);
REQUIRE_EQ(bs.test(2), false);
// resize larger
bs.set(5);
bs.resize(20);
REQUIRE_EQ(bs.size(), 20);
REQUIRE_EQ(bs.test(5), true);
REQUIRE_EQ(bs.count(), 1);
// resize smaller (truncate)
bs.resize(4);
REQUIRE_EQ(bs.size(), 4);
REQUIRE_EQ(bs.test(5), false); // out of range now
REQUIRE_EQ(bs.count(), 0);
// test with larger sizes that span multiple blocks
bitset_dynamic large_bs(100);
large_bs.set(0);
large_bs.set(63);
large_bs.set(64);
large_bs.set(99);
REQUIRE_EQ(large_bs.count(), 4);
REQUIRE_EQ(large_bs.test(0), true);
REQUIRE_EQ(large_bs.test(63), true);
REQUIRE_EQ(large_bs.test(64), true);
REQUIRE_EQ(large_bs.test(99), true);
// flip all bits
bitset_dynamic bs2(5);
for (size_t i = 0; i < 5; ++i)
bs2.set(i, (i % 2) == 0);
bs2.flip();
for (size_t i = 0; i < 5; ++i)
REQUIRE_EQ(bs2.test(i), !((i % 2) == 0));
// all() and count()
bitset_dynamic bs3(4);
for (size_t i = 0; i < 4; ++i)
bs3.set(i);
REQUIRE_EQ(bs3.all(), true);
REQUIRE_EQ(bs3.count(), 4);
// out-of-range ops are no-ops
bs3.set(100);
REQUIRE_EQ(bs3.count(), 4);
// bitwise AND, OR, XOR
bitset_dynamic a(4), b(4);
a.set(0); a.set(2);
b.set(1); b.set(2);
auto or_ab = a | b;
REQUIRE_EQ(or_ab.test(0), true);
REQUIRE_EQ(or_ab.test(1), true);
REQUIRE_EQ(or_ab.test(2), true);
REQUIRE_EQ(or_ab.test(3), false);
auto and_ab = a & b;
REQUIRE_EQ(and_ab.test(2), true);
REQUIRE_EQ(and_ab.test(0), false);
auto xor_ab = a ^ b;
REQUIRE_EQ(xor_ab.test(0), true);
REQUIRE_EQ(xor_ab.test(1), true);
REQUIRE_EQ(xor_ab.test(2), false);
// reset and none()
a.reset(); b.reset();
REQUIRE_EQ(a.none(), true);
REQUIRE_EQ(b.none(), true);
// copy constructor
bitset_dynamic original(10);
original.set(3);
original.set(7);
bitset_dynamic copy(original);
REQUIRE_EQ(copy.size(), 10);
REQUIRE_EQ(copy.test(3), true);
REQUIRE_EQ(copy.test(7), true);
REQUIRE_EQ(copy.count(), 2);
// move constructor
bitset_dynamic moved(fl::move(copy));
REQUIRE_EQ(moved.size(), 10);
REQUIRE_EQ(moved.test(3), true);
REQUIRE_EQ(moved.test(7), true);
REQUIRE_EQ(moved.count(), 2);
REQUIRE_EQ(copy.size(), 0); // moved from should be empty
// assignment operator
bitset_dynamic assigned = original;
REQUIRE_EQ(assigned.size(), 10);
REQUIRE_EQ(assigned.test(3), true);
REQUIRE_EQ(assigned.test(7), true);
// clear
assigned.clear();
REQUIRE_EQ(assigned.size(), 0);
REQUIRE_EQ(assigned.none(), true);
// Test memory size changes with resize
bitset_dynamic small_bs(8);
bitset_dynamic medium_bs(65);
bitset_dynamic large_bs2(129);
// These sizes should match the fixed bitset tests
REQUIRE_EQ(small_bs.size(), 8);
REQUIRE_EQ(medium_bs.size(), 65);
REQUIRE_EQ(large_bs2.size(), 129);
}

View file

@ -0,0 +1,177 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "lib8tion/intmap.h"
#include "lib8tion/brightness_bitshifter.h"
#include <iostream>
#include <bitset>
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
TEST_CASE("brightness_bitshifter8") {
SUBCASE("random test to check that the product is the same") {
int count = 0;
for (int i = 0; i < 10000; ++i) {
uint8_t brightness_src = 0b10000000 >> rand() % 6;
uint8_t brightness_dst = rand() % 256;
uint16_t product = uint16_t(brightness_src) * brightness_dst;
uint8_t shifts = brightness_bitshifter8(&brightness_src, &brightness_dst, 7);
uint16_t new_product = uint16_t(brightness_src) * brightness_dst;
CHECK_EQ(product, new_product);
if (shifts) {
count++;
}
}
CHECK_GT(count, 0);
}
SUBCASE("fixed test data mimicing actual use") {
const uint8_t test_data[][2][2] = {
// brightness_bitshifter8 is always called with brightness_src = 0b00010000
{ // test case
// src dst
{0b00010000, 0b00000000}, // input
{0b00010000, 0b00000000}, // output
},
{
{0b00010000, 0b00000001},
{0b00000001, 0b00010000},
},
{
{0b00010000, 0b00000100},
{0b00000001, 0b01000000},
},
{
{0b00010000, 0b00010000},
{0b00000010, 0b10000000},
},
{
{0b00010000, 0b00001010},
{0b00000001, 0b10100000},
},
{
{0b00010000, 0b00101010},
{0b00000100, 0b10101000},
},
{
{0b00010000, 0b11101010},
{0b00010000, 0b11101010},
},
};
for (const auto& data : test_data) {
uint8_t brightness_src = data[0][0];
uint8_t brightness_dst = data[0][1];
uint8_t shifts = brightness_bitshifter8(&brightness_src, &brightness_dst, 4);
INFO("input brightness_src: ", data[0][0], " ; input brightness_dst: ", data[0][1]);
INFO("output brightness_src: ", brightness_src, " ; output brightness_dst: ", brightness_dst);
INFO("shifts : ", shifts);
CHECK_EQ(brightness_src, data[1][0]);
CHECK_EQ(brightness_dst, data[1][1]);
}
}
}
TEST_CASE("brightness_bitshifter16") {
SUBCASE("simple with steps=2") {
uint8_t brightness_src = 0x1 << 1;
uint16_t brightness_dst = 0x1 << 2;
uint8_t max_shifts = 8;
uint8_t shifts = brightness_bitshifter16(&brightness_src, &brightness_dst, max_shifts, 2);
CHECK_EQ(shifts, 1);
CHECK_EQ(brightness_src, 1);
CHECK_EQ(brightness_dst, 0x1 << 4);
}
SUBCASE("simple with steps=1") {
uint8_t brightness_src = 0x1 << 1;
uint16_t brightness_dst = 0x1 << 1;
uint8_t max_shifts = 8;
uint8_t shifts = brightness_bitshifter16(&brightness_src, &brightness_dst, max_shifts, 1);
CHECK_EQ(shifts, 1);
CHECK_EQ(brightness_src, 1);
CHECK_EQ(brightness_dst, 0x1 << 2);
}
SUBCASE("random test to check that the product is the same") {
int count = 0;
for (int i = 0; i < 10000; ++i) {
uint8_t brightness_src = 0b10000000 >> (rand() % 8);
uint16_t brightness_dst = rand() % uint32_t(65536);
uint32_t product = uint32_t(brightness_src >> 8) * brightness_dst;
uint8_t max_shifts = 8;
uint8_t steps = 2;
uint8_t shifts = brightness_bitshifter16(&brightness_src, &brightness_dst, max_shifts, steps);
uint32_t new_product = uint32_t(brightness_src >> 8) * brightness_dst;
CHECK_EQ(product, new_product);
if (shifts) {
count++;
}
}
CHECK_GT(count, 0);
}
SUBCASE("fixed test data mimicing actual use") {
const uint16_t test_data[][2][2] = {
// brightness_bitshifter16 is always called with brightness_src between 0b00000001 - 0b00010000
{ // test case
// src dst
{0b00000001, 0b0000000000000000}, // input
{0b00000001, 0b0000000000000000}, // output
},
{
{0b00000001, 0b0000000000000001},
{0b00000001, 0b0000000000000001},
},
{
{0b00000001, 0b0000000000000010},
{0b00000001, 0b0000000000000010},
},
{
{0b00000010, 0b0000000000000001},
{0b00000001, 0b0000000000000100},
},
{
{0b00001010, 0b0000000000001010},
{0b00000101, 0b0000000000101000},
},
{
{0b00010000, 0b0000111000100100},
{0b00000100, 0b1110001001000000},
},
{
{0b00010000, 0b0011100010010010},
{0b00001000, 0b1110001001001000},
},
{
{0b00010000, 0b0110001001001110},
{0b00010000, 0b0110001001001110},
},
{
{0b00010000, 0b1110001001001110},
{0b00010000, 0b1110001001001110},
},
};
for (const auto& data : test_data) {
uint8_t brightness_src = static_cast<uint8_t>(data[0][0]);
uint16_t brightness_dst = data[0][1];
uint8_t shifts = brightness_bitshifter16(&brightness_src, &brightness_dst, 4, 2);
INFO("input brightness_src: ", data[0][0], " ; input brightness_dst: ", data[0][1]);
INFO("output brightness_src: ", brightness_src, " ; output brightness_dst: ", brightness_dst);
INFO("shifts (by 2 bits): ", shifts);
CHECK_EQ(brightness_src, data[1][0]);
CHECK_EQ(brightness_dst, data[1][1]);
}
}
}

View file

@ -0,0 +1,261 @@
// Compile with: g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/bytestreammemory.h"
#include "fx/video/pixel_stream.h"
#include "fl/namespace.h"
using namespace fl;
TEST_CASE("ByteStreamMemory basic operations") {
SUBCASE("Write and read single byte") {
ByteStreamMemory stream(10); // Stream with 10 bytes capacity
uint8_t testByte = 42;
CHECK(stream.write(&testByte, 1) == 1);
uint8_t readByte = 0;
CHECK(stream.read(&readByte, 1) == 1);
CHECK(readByte == testByte);
// Next read should fail since the stream is empty
CHECK(stream.read(&readByte, 1) == 0);
}
SUBCASE("Write and read multiple bytes") {
ByteStreamMemory stream(10);
uint8_t testData[] = {1, 2, 3, 4, 5};
CHECK(stream.write(testData, 5) == 5);
uint8_t readData[5] = {0};
CHECK(stream.read(readData, 5) == 5);
for (int i = 0; i < 5; ++i) {
CHECK(readData[i] == testData[i]);
}
}
SUBCASE("Attempt to read from empty stream") {
ByteStreamMemory stream(10);
uint8_t readByte = 0;
CHECK(stream.read(&readByte, 1) == 0);
}
SUBCASE("Attempt to write beyond capacity") {
ByteStreamMemory stream(5);
uint8_t testData[] = {1, 2, 3, 4, 5, 6};
CHECK(stream.write(testData, 6) == 5); // Should write up to capacity
}
SUBCASE("Attempt to read more than available data") {
ByteStreamMemory stream(10);
uint8_t testData[] = {1, 2, 3};
CHECK(stream.write(testData, 3) == 3);
uint8_t readData[5] = {0};
CHECK_FALSE(stream.read(readData, 5) == 3); // Should read only available data
//CHECK(readData[0] == 1);
//CHECK(readData[1] == 2);
//CHECK(readData[2] == 3);
}
SUBCASE("Multiple write and read operations") {
ByteStreamMemory stream(10);
uint8_t testData1[] = {1, 2, 3};
uint8_t testData2[] = {4, 5};
CHECK(stream.write(testData1, 3) == 3);
CHECK(stream.write(testData2, 2) == 2);
uint8_t readData[5] = {0};
CHECK(stream.read(readData, 5) == 5);
CHECK(readData[0] == 1);
CHECK(readData[1] == 2);
CHECK(readData[2] == 3);
CHECK(readData[3] == 4);
CHECK(readData[4] == 5);
}
SUBCASE("Write after partial read") {
ByteStreamMemory stream(10);
uint8_t testData[] = {1, 2, 3, 4, 5};
CHECK(stream.write(testData, 5) == 5);
uint8_t readData[2] = {0};
CHECK(stream.read(readData, 2) == 2);
CHECK(readData[0] == 1);
CHECK(readData[1] == 2);
uint8_t newTestData[] = {6, 7};
CHECK(stream.write(newTestData, 2) == 2);
uint8_t remainingData[5] = {0};
CHECK(stream.read(remainingData, 5) == 5);
CHECK(remainingData[0] == 3);
CHECK(remainingData[1] == 4);
CHECK(remainingData[2] == 5);
CHECK(remainingData[3] == 6);
CHECK(remainingData[4] == 7);
}
SUBCASE("Fill and empty stream multiple times") {
ByteStreamMemory stream(10);
uint8_t testData[10];
for (uint8_t i = 0; i < 10; ++i) {
testData[i] = i;
}
// First cycle
CHECK(stream.write(testData, 10) == 10);
uint8_t readData[10] = {0};
CHECK(stream.read(readData, 10) == 10);
for (uint8_t i = 0; i < 10; ++i) {
CHECK(readData[i] == i);
}
// Second cycle
CHECK(stream.write(testData, 10) == 10);
CHECK(stream.read(readData, 10) == 10);
for (uint8_t i = 0; i < 10; ++i) {
CHECK(readData[i] == i);
}
}
SUBCASE("Zero-length write and read") {
ByteStreamMemory stream(10);
uint8_t testData[] = {1, 2, 3};
CHECK(stream.write(testData, 0) == 0);
uint8_t readData[3] = {0};
CHECK(stream.read(readData, 0) == 0);
}
SUBCASE("Write and read with null pointers") {
ByteStreamMemory stream(10);
CHECK(stream.write(static_cast<uint8_t*>(nullptr), 5) == 0);
CHECK(stream.read(nullptr, 5) == 0);
}
SUBCASE("Boundary conditions") {
ByteStreamMemory stream(10);
uint8_t testData[10];
for (uint8_t i = 0; i < 10; ++i) {
testData[i] = i;
}
CHECK(stream.write(testData, 10) == 10);
uint8_t readData[10] = {0};
CHECK(stream.read(readData, 10) == 10);
for (uint8_t i = 0; i < 10; ++i) {
CHECK(readData[i] == i);
}
// Try to write again to full capacity
CHECK(stream.write(testData, 10) == 10);
}
SUBCASE("Write with partial capacity") {
ByteStreamMemory stream(5);
uint8_t testData[] = {1, 2, 3, 4, 5};
CHECK(stream.write(testData, 5) == 5);
uint8_t moreData[] = {6, 7};
CHECK(stream.write(moreData, 2) == 0); // Should not write since capacity is full
uint8_t readData[5] = {0};
CHECK(stream.read(readData, 5) == 5);
for (int i = 0; i < 5; ++i) {
CHECK(readData[i] == testData[i]);
}
// Now buffer is empty, try writing again
CHECK(stream.write(moreData, 2) == 2);
CHECK(stream.read(readData, 2) == 2);
CHECK(readData[0] == 6);
CHECK(readData[1] == 7);
}
SUBCASE("Read after buffer is reset") {
ByteStreamMemory stream(10);
uint8_t testData[] = {1, 2, 3};
CHECK(stream.write(testData, 3) == 3);
stream.clear(); // Assuming reset clears the buffer
uint8_t readData[3] = {0};
CHECK(stream.read(readData, 3) == 0); // Should read nothing
}
SUBCASE("Write zero bytes when buffer is full") {
ByteStreamMemory stream(0); // Zero capacity
uint8_t testByte = 42;
CHECK(stream.write(&testByte, 1) == 0); // Cannot write to zero-capacity buffer
}
SUBCASE("Sequential writes and reads") {
ByteStreamMemory stream(10);
for (uint8_t i = 0; i < 10; ++i) {
CHECK(stream.write(&i, 1) == 1);
}
uint8_t readByte = 0;
for (uint8_t i = 0; i < 10; ++i) {
CHECK(stream.read(&readByte, 1) == 1);
CHECK(readByte == i);
}
// Stream should now be empty
CHECK(stream.read(&readByte, 1) == 0);
}
}
TEST_CASE("byte stream memory basic operations") {
const int BYTES_PER_FRAME = 3 * 10 * 10; // Assuming a 10x10 RGB video
// Create a ByteStreamMemory
const uint32_t BUFFER_SIZE = BYTES_PER_FRAME * 10; // Enough for 10 frames
ByteStreamMemoryPtr memoryStream = ByteStreamMemoryPtr::New(BUFFER_SIZE);
// Fill the ByteStreamMemory with test data
uint8_t testData[BUFFER_SIZE];
for (uint32_t i = 0; i < BUFFER_SIZE; ++i) {
testData[i] = static_cast<uint8_t>(i % 256);
}
memoryStream->write(testData, BUFFER_SIZE);
// Create and initialize PixelStream
PixelStreamPtr stream = PixelStreamPtr::New(BYTES_PER_FRAME);
bool initSuccess = stream->beginStream(memoryStream);
REQUIRE(initSuccess);
// Test basic properties
CHECK(stream->getType() == PixelStream::kStreaming);
CHECK(stream->bytesPerFrame() == BYTES_PER_FRAME);
// Read a pixel
CRGB pixel;
bool readSuccess = stream->readPixel(&pixel);
REQUIRE(readSuccess);
CHECK(pixel.r == 0);
CHECK(pixel.g == 1);
CHECK(pixel.b == 2);
// Read some bytes
uint8_t buffer[10];
size_t bytesRead = stream->readBytes(buffer, 10);
CHECK(bytesRead == 10);
for (int i = 0; i < 10; ++i) {
CHECK(buffer[i] == static_cast<uint8_t>((i + 3) % 256));
}
// Check frame counting - streaming mode doesn't support this.
//CHECK(PixelStream->framesDisplayed() == 0);
//CHECK(PixelStream->framesRemaining() == 10); // We have 10 frames of data
// Close the stream
stream->close();
}

View file

@ -0,0 +1,297 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/circular_buffer.h"
#include "fl/namespace.h"
using namespace fl;
TEST_CASE("circular_buffer basic operations") {
CircularBuffer<int> buffer(5);
CHECK(buffer.empty());
CHECK_EQ(buffer.size(), 0);
buffer.push_back(1);
buffer.push_back(2);
buffer.push_back(3);
CHECK_EQ(buffer.size(), 3);
CHECK_FALSE(buffer.empty());
CHECK_FALSE(buffer.full());
CHECK_EQ(buffer.front(), 1);
CHECK_EQ(buffer.back(), 3);
int value;
CHECK_EQ(buffer.pop_front(&value), true);
CHECK_EQ(value, 1);
CHECK_EQ(buffer.size(), 2);
CHECK_EQ(buffer.front(), 2);
}
TEST_CASE("circular_buffer operator[]") {
CircularBuffer<int> buffer(5);
CHECK(buffer.empty());
CHECK_EQ(buffer.size(), 0);
buffer.push_back(1);
buffer.push_back(2);
CHECK_EQ(buffer.size(), 2);
CHECK_EQ(buffer[0], 1);
CHECK_EQ(buffer[1], 2);
buffer.pop_front(nullptr);
CHECK_EQ(2, buffer[0]);
buffer.push_back(3);
CHECK_EQ(2, buffer[0]);
CHECK_EQ(3, buffer[1]);
buffer.pop_back(nullptr);
CHECK_EQ(2, buffer[0]);
}
TEST_CASE("circular_buffer overflow behavior") {
CircularBuffer<int> buffer(3);
buffer.push_back(1);
buffer.push_back(2);
buffer.push_back(3);
CHECK(buffer.full());
buffer.push_back(4);
CHECK(buffer.full());
CHECK_EQ(buffer.size(), 3);
int value;
CHECK_EQ(buffer.pop_front(&value), true);
CHECK_EQ(value, 2);
CHECK_EQ(buffer.pop_front(&value), true);
CHECK_EQ(value, 3);
CHECK_EQ(buffer.pop_front(&value), true);
CHECK_EQ(value, 4);
CHECK(buffer.empty());
// CHECK_EQ(buffer.pop_front(), 0); // Returns default-constructed int (0) when empty
CHECK_EQ(buffer.pop_front(&value), false);
}
TEST_CASE("circular_buffer edge cases") {
CircularBuffer<int> buffer(1);
CHECK(buffer.empty());
CHECK_FALSE(buffer.full());
buffer.push_back(42);
CHECK_FALSE(buffer.empty());
CHECK(buffer.full());
buffer.push_back(43);
CHECK_EQ(buffer.front(), 43);
CHECK_EQ(buffer.back(), 43);
int value;
bool ok = buffer.pop_front(&value);
CHECK(ok);
CHECK_EQ(value, 43);
CHECK(buffer.empty());
}
TEST_CASE("circular_buffer clear operation") {
CircularBuffer<int> buffer(5);
buffer.push_back(1);
buffer.push_back(2);
buffer.push_back(3);
CHECK_EQ(buffer.size(), 3);
buffer.clear();
CHECK(buffer.empty());
CHECK_EQ(buffer.size(), 0);
buffer.push_back(4);
CHECK_EQ(buffer.front(), 4);
CHECK_EQ(buffer.back(), 4);
}
TEST_CASE("circular_buffer indexing") {
CircularBuffer<int> buffer(5);
buffer.push_back(10);
buffer.push_back(20);
buffer.push_back(30);
CHECK_EQ(buffer[0], 10);
CHECK_EQ(buffer[1], 20);
CHECK_EQ(buffer[2], 30);
buffer.pop_front(nullptr);
buffer.push_back(40);
CHECK_EQ(buffer[0], 20);
CHECK_EQ(buffer[1], 30);
CHECK_EQ(buffer[2], 40);
}
TEST_CASE("circular_buffer with custom type") {
struct CustomType {
int value;
CustomType(int v = 0) : value(v) {}
bool operator==(const CustomType& other) const { return value == other.value; }
};
CircularBuffer<CustomType> buffer(3);
buffer.push_back(CustomType(1));
buffer.push_back(CustomType(2));
buffer.push_back(CustomType(3));
CHECK_EQ(buffer.front().value, 1);
CHECK_EQ(buffer.back().value, 3);
buffer.push_back(CustomType(4));
CustomType value;
CHECK_EQ(buffer.pop_front(&value), true);
CHECK_EQ(value.value, 2);
CHECK_EQ(buffer.pop_front(&value), true);
CHECK_EQ(value.value, 3);
CHECK_EQ(buffer.pop_front(&value), true);
CHECK_EQ(value.value, 4);
}
TEST_CASE("circular_buffer writing to full buffer") {
CircularBuffer<int> buffer(3);
// Fill the buffer
buffer.push_back(1);
buffer.push_back(2);
buffer.push_back(3);
CHECK(buffer.full());
// Write to full buffer
buffer.push_back(4);
CHECK(buffer.full());
CHECK_EQ(buffer.size(), 3);
// Check that the oldest element was overwritten
CHECK_EQ(buffer[0], 2);
CHECK_EQ(buffer[1], 3);
CHECK_EQ(buffer[2], 4);
// Write multiple elements to full buffer
buffer.push_back(5);
buffer.push_back(6);
CHECK(buffer.full());
CHECK_EQ(buffer.size(), 3);
// Check that the buffer contains only the most recent elements
CHECK_EQ(buffer[0], 4);
CHECK_EQ(buffer[1], 5);
CHECK_EQ(buffer[2], 6);
// Verify front() and back()
CHECK_EQ(buffer.front(), 4);
CHECK_EQ(buffer.back(), 6);
// Pop all elements and verify
//CHECK_EQ(buffer.pop_front(), 4);
//CHECK_EQ(buffer.pop_front(), 5);
//CHECK_EQ(buffer.pop_front(), 6);
int value;
CHECK_EQ(buffer.pop_front(&value), true);
CHECK_EQ(value, 4);
CHECK_EQ(buffer.pop_front(&value), true);
CHECK_EQ(value, 5);
CHECK_EQ(buffer.pop_front(&value), true);
CHECK_EQ(value, 6);
CHECK(buffer.empty());
}
#if 1
TEST_CASE("circular_buffer zero capacity") {
CircularBuffer<int> buffer(0);
CHECK(buffer.empty());
CHECK(buffer.full());
CHECK_EQ(buffer.size(), 0);
// Attempt to push an element
buffer.push_back(1);
// Buffer should now contain one element
CHECK(buffer.empty());
CHECK(buffer.full());
CHECK_EQ(buffer.size(), 0);
// Attempt to pop an element
// CHECK_EQ(buffer.pop_front(), 0);
int value;
CHECK_EQ(buffer.pop_front(&value), false);
// Buffer should be empty again
CHECK(buffer.empty());
CHECK(buffer.full());
CHECK_EQ(buffer.size(), 0);
}
#endif
TEST_CASE("circular_buffer pop_back operation") {
CircularBuffer<int> buffer(5);
buffer.push_back(1);
buffer.push_back(2);
buffer.push_back(3);
int value;
CHECK_EQ(buffer.pop_back(&value), true);
CHECK_EQ(value, 3);
CHECK_EQ(buffer.size(), 2);
CHECK_EQ(buffer.back(), 2);
CHECK_EQ(buffer.pop_back(&value), true);
CHECK_EQ(value, 2);
CHECK_EQ(buffer.size(), 1);
CHECK_EQ(buffer.front(), 1);
CHECK_EQ(buffer.back(), 1);
CHECK_EQ(buffer.pop_back(&value), true);
CHECK_EQ(value, 1);
CHECK(buffer.empty());
CHECK_EQ(buffer.pop_back(&value), false);
}
TEST_CASE("circular_buffer push_front operation") {
CircularBuffer<int> buffer(3);
buffer.push_front(1);
buffer.push_front(2);
buffer.push_front(3);
CHECK_EQ(buffer.size(), 3);
CHECK_EQ(buffer.front(), 3);
CHECK_EQ(buffer.back(), 1);
buffer.push_front(4);
CHECK_EQ(buffer.size(), 3);
CHECK_EQ(buffer.front(), 4);
CHECK_EQ(buffer.back(), 2);
int value;
CHECK_EQ(buffer.pop_back(&value), true);
CHECK_EQ(value, 2);
CHECK_EQ(buffer.pop_back(&value), true);
CHECK_EQ(value, 3);
CHECK_EQ(buffer.pop_back(&value), true);
CHECK_EQ(value, 4);
CHECK(buffer.empty());
}

View file

@ -0,0 +1,139 @@
// g++ --std=c++11 test.cpp
#include "fl/math_macros.h"
#include "test.h"
#include "fl/sstream.h"
#include "fl/corkscrew.h"
#define NUM_LEDS 288
#define TWO_PI (PI * 2.0)
// Define an improved CHECK_CLOSE macro that provides better error messages
#define CHECK_CLOSE(a, b, epsilon) \
do { \
float _a = (a); \
float _b = (b); \
float _diff = fabsf(_a - _b); \
bool _result = _diff <= (epsilon); \
if (!_result) { \
printf("CHECK_CLOSE failed: |%f - %f| = %f > %f\n", (float)_a, \
(float)_b, _diff, (float)(epsilon)); \
} \
CHECK(_result); \
} while (0)
using namespace fl;
TEST_CASE("Corkscrew generateMap") {
Corkscrew::Input input;
input.totalHeight = 10.0f;
input.totalAngle = TWO_PI;
input.offsetCircumference = 0.0f;
input.numLeds = 10;
Corkscrew::Output output = Corkscrew::generateMap(input);
CHECK_EQ(output.width, 10);
CHECK_EQ(output.height, 1); // One vertical segment for one turn
CHECK_EQ(output.mapping.size(), 10); // 10 LEDs around the corkscrew
CHECK_GE(output.mapping[0].x, 0.0f);
CHECK_LE(output.mapping[0].x, 10.0f);
CHECK_GE(output.mapping[0].y, 0.0f);
CHECK_LE(output.mapping[0].y, 1.0f); // 1 vertical segment for 2π angle
}
TEST_CASE("Corkscrew to Frame Buffer Mapping") {
// Define the corkscrew input parameters
const int kCorkscrewTotalHeight = 1; // cm
//const int CORKSCREW_WIDTH = 1; // Width of the corkscrew in pixels
//const int CORKSCREW_HEIGHT = 1; // Height of the corkscrew in pixels
const int kCorkscrewTurns = 2; // Default to 19 turns
Corkscrew::Input input;
input.totalHeight = kCorkscrewTotalHeight;
input.totalAngle = kCorkscrewTurns * 2 * PI; // Default to 19 turns
input.offsetCircumference = 0.0f;
input.numLeds = 3;
// Generate the corkscrew map
Corkscrew corkscrew(input);
volatile Corkscrew::Output* output = &corkscrew.access();
// vec2<int16_t> first = corkscrew.at(0);
// vec2<int16_t> second = corkscrew.at(1);
Corkscrew::iterator it = corkscrew.begin();
Corkscrew::iterator end = corkscrew.end();
fl::sstream ss;
ss << "\n";
ss << "width: " << output->width << "\n";
ss << "height: " << output->height << "\n";
while (it != end) {
ss << *it << "\n";
++it;
}
FASTLED_WARN(ss.str());
MESSAGE("done");
}
TEST_CASE("Corkscrew generateMap with two turns") {
Corkscrew::Input input;
input.totalHeight = 10.0f;
input.totalAngle = 2 * TWO_PI; // Two full turns
input.numLeds = 10; // 10 LEDs around the corkscrew
input.offsetCircumference = 0.0f;
Corkscrew::Output output = Corkscrew::generateMap(input);
CHECK_EQ(output.width, 5);
CHECK_EQ(output.height, 2); // Two vertical segments for two turns
CHECK_EQ(output.mapping.size(), 10); // 5 width * 2 height
// Check first pixel for correctness (basic integrity)
CHECK_GE(output.mapping[0].x, 0.0f);
CHECK_LE(output.mapping[0].x, 5.0f);
CHECK_GE(output.mapping[0].y, 0.0f);
CHECK_LE(output.mapping[0].y, 2.0f); // 2 vertical segments for 4π angle
}
TEST_CASE("Corkscrew circumference test") {
Corkscrew::Input input;
// Use defaults: totalHeight = 100, totalAngle = 19 * 2 * PI
input.totalHeight = 23.25f; // Total height of the corkscrew in centimeters
input.totalAngle = 19.0f * TWO_PI; // Default to 19 turns
input.offsetCircumference = 0.0f; // No offset
input.numLeds = 288; // Default to dense 144 LEDs times two strips
Corkscrew::Output output = Corkscrew::generateMap(input);
// Basic sanity checks
CHECK_EQ(output.width, 16);
CHECK_EQ(output.height, 19);
CHECK_EQ(output.mapping.size(), 288);
// Check that circumference matches calculated value
// float expectedCircumference = 100.0f / 19.0f;
// CHECK_CLOSE(output.circumference, expectedCircumference, 0.01f);
}

View file

@ -0,0 +1,20 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/dbg.h"
#include "fl/namespace.h"
using namespace fl;
TEST_CASE("fastled_file_offset") {
const char* src = "src/fl/dbg.h";
const char* result = fastled_file_offset(src);
CHECK(strcmp(result, "src/fl/dbg.h") == 0);
const char* src2 = "blah/blah/blah.h";
const char* result2 = fastled_file_offset(src2);
CHECK(strcmp(result2, "blah.h") == 0);
}

View file

@ -0,0 +1,127 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "fl/downscale.h"
#include "fl/dbg.h"
#include "test.h"
using namespace fl;
TEST_CASE("downscale 2x2 to 1x1") {
CRGB red = CRGB(255, 0, 0);
CRGB black = CRGB(0, 0, 0);
// We are going to simulate a 4x4 image with a 2x2 image. The source
// image is square-cartesian while the dst image is square-serpentine.
CRGB src[4] = {black, red, black, red};
SUBCASE("downscaleHalf from 2x2 to 1x1") {
CRGB dst[1];
downscaleHalf(src, 2, 2, dst);
INFO("Src: " << src);
INFO("Dst: " << dst);
CHECK(dst[0].r == 128);
CHECK(dst[0].g == 0);
CHECK(dst[0].b == 0);
}
SUBCASE("downscale from 2x2 to 1x1") {
CRGB dst[1];
XYMap srcMap = XYMap::constructRectangularGrid(2, 2);
XYMap dstMap = XYMap::constructRectangularGrid(1, 1);
downscale(src, srcMap, dst, dstMap);
INFO("Src: " << src);
INFO("Dst: " << dst);
CHECK(dst[0].r == 128);
CHECK(dst[0].g == 0);
CHECK(dst[0].b == 0);
}
SUBCASE("4x4 rectangle to 2x2 serpentine") {
// We are going to simulate a 4x4 image with a 2x2 image. The source
// image is square-cartesian while the dst image is square-serpentine.
CRGB src[16] = {// Upper left red, lower right red, upper right black,
// lower left black
red, red, black, black, red, red, black, black,
black, black, red, red, black, black, red, red};
CRGB dst[4];
XYMap srcMap = XYMap::constructRectangularGrid(4, 4);
XYMap dstMap = XYMap::constructSerpentine(2, 2);
downscale(src, srcMap, dst, dstMap);
INFO("Src: " << src);
INFO("Dst: " << dst);
CRGB lowerLeft = dst[dstMap.mapToIndex(0, 0)];
CRGB lowerRight = dst[dstMap.mapToIndex(1, 0)];
CRGB upperLeft = dst[dstMap.mapToIndex(0, 1)];
CRGB upperRight = dst[dstMap.mapToIndex(1, 1)];
REQUIRE(lowerLeft == red);
REQUIRE(lowerRight == black);
REQUIRE(upperLeft == black);
REQUIRE(upperRight == red);
}
}
TEST_CASE("downscale 3x3 to 2x2") {
CRGB red = CRGB(255, 0, 0);
CRGB black = CRGB(0, 0, 0);
// Create a 3x3 checkerboard pattern:
CRGB src[9];
src[0] = red;
src[1] = black;
src[2] = red;
src[3] = black;
src[4] = red;
src[5] = black;
src[6] = red;
src[7] = black;
src[8] = red;
CRGB dst[4]; // 2x2 result
XYMap srcMap = XYMap::constructRectangularGrid(3, 3);
XYMap dstMap = XYMap::constructRectangularGrid(2, 2);
downscale(src, srcMap, dst, dstMap);
for (int i = 0; i < 4; ++i) {
INFO("Dst[" << i << "]: " << dst[i]);
CHECK(dst[i] == CRGB(142, 0, 0)); // Averaged color
}
}
TEST_CASE("downscale 11x11 to 2x2") {
CRGB red = CRGB(255, 0, 0);
CRGB black = CRGB(0, 0, 0);
// Create a 3x3 checkerboard pattern:
CRGB src[11*11];
for (int i = 0; i < 11*11; ++i) {
src[i] = (i % 2 == 0) ? red : black;
}
CRGB dst[4]; // 2x2 result
XYMap srcMap = XYMap::constructRectangularGrid(11, 11);
XYMap dstMap = XYMap::constructRectangularGrid(2, 2);
downscale(src, srcMap, dst, dstMap);
for (int i = 0; i < 4; ++i) {
INFO("Dst[" << i << "]: " << dst[i]);
CHECK(dst[i] == CRGB(129, 0, 0)); // Averaged color
}
}

View file

@ -0,0 +1,21 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "FastLED.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
#define NUM_LEDS 1000
#define DATA_PIN 2
#define CLOCK_PIN 3
CRGB leds[NUM_LEDS];
TEST_CASE("Simple") {
FastLED.addLeds<APA102, DATA_PIN, CLOCK_PIN, BGR>(leds, NUM_LEDS);
}

View file

@ -0,0 +1,142 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/fft.h"
#include "fl/fft_impl.h"
#include "fl/math.h"
// // Proof of concept FFTImpl using KISS FFTImpl. Right now this is fixed sized blocks
// of 512. But this is
// // intended to change with a C++ wrapper around ot/
// typedef int16_t fft_audio_buffer_t[512];
// void fft_init(); // Explicit initialization of FFTImpl, otherwise it will be
// initialized on first run. bool fft_is_initialized(); void fft_unit_test(const
// fft_audio_buffer_t &buffer);
using namespace fl;
TEST_CASE("fft tester 512") {
int16_t buffer[512] = {0};
const int n = 512;
// fill in with a sine wave
for (int i = 0; i < n; ++i) {
float rot = fl::map_range<float, float>(i, 0, n - 1, 0, 2 * PI * 10);
float sin_x = sin(rot);
buffer[i] = int16_t(32767 * sin_x);
}
FFTBins out(16);
// fft_unit_test(buffer, &out);
const int samples = n;
FFTImpl fft(samples);
fft.run(buffer, &out);
FASTLED_WARN("FFTImpl output raw bins: " << out.bins_raw);
FASTLED_WARN("FFTImpl output db bins: " << out.bins_db);
const float expected_output[16] = {
3, 2, 2, 6, 6.08, 15.03, 3078.22, 4346.29,
4033.16, 3109, 38.05, 4.47, 4, 2, 1.41, 1.41};
for (int i = 0; i < 16; ++i) {
// CHECK(out[i] == Approx(expected_output[i]).epsilon(0.1));
float a = out.bins_raw[i];
float b = expected_output[i];
bool almost_equal = ALMOST_EQUAL(a, b, 0.1);
if (!almost_equal) {
FASTLED_WARN("FFTImpl output mismatch at index " << i << ": " << a
<< " != " << b);
}
CHECK(almost_equal);
}
fl::Str info = fft.info();
FASTLED_WARN("FFTImpl info: " << info);
FASTLED_WARN("Done");
}
TEST_CASE("fft tester 256") {
// fft_audio_buffer_t buffer = {0};
fl::vector<int16_t> buffer;
const int n = 256;
// fill in with a sine wave
for (int i = 0; i < n; ++i) {
float rot = fl::map_range<float, float>(i, 0, n - 1, 0, 2 * PI * 10);
float sin_x = sin(rot);
auto v = int16_t(32767 * sin_x);
buffer.push_back(v);
}
FFTBins out(16);
// fft_unit_test(buffer, &out);
const int samples = n;
FFTImpl fft(samples);
fft.run(buffer, &out);
FASTLED_WARN("FFTImpl output: " << out);
const float expected_output[16] = {
3, 2, 4, 5, 5.10, 9.06, 11.05, 27.66,
2779.93, 3811.66, 4176.58, 4185.02, 4174.50, 4017.63, 3638.46, 3327.60};
for (int i = 0; i < 16; ++i) {
// CHECK(out[i] == Approx(expected_output[i]).epsilon(0.1));
float a = out.bins_raw[i];
float b = expected_output[i];
bool almost_equal = ALMOST_EQUAL(a, b, 0.1);
if (!almost_equal) {
FASTLED_WARN("FFTImpl output mismatch at index " << i << ": " << a
<< " != " << b);
}
CHECK(almost_equal);
}
fl::Str info = fft.info();
FASTLED_WARN("FFTImpl info: " << info);
FASTLED_WARN("Done");
}
TEST_CASE("fft tester 256 with 64 bands") {
// fft_audio_buffer_t buffer = {0};
fl::vector<int16_t> buffer;
const int n = 256;
// fill in with a sine wave
for (int i = 0; i < n; ++i) {
float rot = fl::map_range<float, float>(i, 0, n - 1, 0, 2 * PI * 10);
float sin_x = sin(rot);
auto v = int16_t(32767 * sin_x);
buffer.push_back(v);
}
FFTBins out(64);
// fft_unit_test(buffer, &out);
const int samples = n;
FFT_Args args(samples, 64);
FFTImpl fft(args);
fft.run(buffer, &out);
FASTLED_WARN("FFTImpl output: " << out);
const float expected_output[64] = {
3, 3, 1, 2, 2, 3, 3,
3, 3, 4, 3, 4, 4, 5,
5, 3.16, 4.12, 5.10, 5.10, 6.08, 7,
9.06, 9.06, 9.06, 10.20, 11.18, 15.13, 18.25,
20.22, 26.31, 30.59, 63.95, 71.85, 2601.78, 2896.46,
3281.87, 3473.71, 3678.96, 3876.88, 3960.81, 4023.50, 4203.33,
4176.58, 4286.66, 4199.48, 4273.51, 4274.82, 4155.52, 4170.46,
4121.19, 4131.86, 4044.44, 4072.49, 4120.38, 3966.60, 4017.84,
3815.20, 3815.66, 3964.51, 3628.27, 3599.13, 3863.29, 3823.06,
3327.600};
for (int i = 0; i < 64; ++i) {
// CHECK(out[i] == Approx(expected_output[i]).epsilon(0.1));
float a = out.bins_raw[i];
float b = expected_output[i];
bool almost_equal = ALMOST_EQUAL(a, b, 0.1);
if (!almost_equal) {
FASTLED_WARN("FFTImpl output mismatch at index " << i << ": " << a
<< " != " << b);
}
CHECK(almost_equal);
}
fl::Str info = fft.info();
FASTLED_WARN("FFTImpl info: " << info);
FASTLED_WARN("Done");
}

View file

@ -0,0 +1,97 @@
// g++ --std=c++11 test_fixed_map.cpp -I../src
#include "test.h"
#include "test.h"
#include "fl/set.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
TEST_CASE("FixedSet operations") {
fl::FixedSet<int, 5> set;
SUBCASE("Insert and find") {
CHECK(set.insert(1));
CHECK(set.insert(2));
CHECK(set.insert(3));
CHECK(set.find(1) != set.end());
CHECK(set.find(2) != set.end());
CHECK(set.find(3) != set.end());
CHECK(set.find(4) == set.end());
CHECK_FALSE(set.insert(1)); // Duplicate insert should fail
}
SUBCASE("Erase") {
CHECK(set.insert(1));
CHECK(set.insert(2));
CHECK(set.erase(1));
CHECK(set.find(1) == set.end());
CHECK(set.find(2) != set.end());
CHECK_FALSE(set.erase(3)); // Erasing non-existent element should fail
}
SUBCASE("Next and prev") {
CHECK(set.insert(1));
CHECK(set.insert(2));
CHECK(set.insert(3));
int next_value;
CHECK(set.next(1, &next_value));
CHECK(next_value == 2);
CHECK(set.next(3, &next_value, true));
CHECK(next_value == 1);
int prev_value;
CHECK(set.prev(3, &prev_value));
CHECK(prev_value == 2);
CHECK(set.prev(1, &prev_value, true));
CHECK(prev_value == 3);
}
SUBCASE("Size and capacity") {
CHECK(set.size() == 0);
CHECK(set.capacity() == 5);
CHECK(set.empty());
set.insert(1);
set.insert(2);
CHECK(set.size() == 2);
CHECK_FALSE(set.empty());
set.clear();
CHECK(set.size() == 0);
CHECK(set.empty());
}
SUBCASE("Iterators") {
set.insert(1);
set.insert(2);
set.insert(3);
int sum = 0;
for (const auto& value : set) {
sum += value;
}
CHECK(sum == 6);
auto it = set.begin();
CHECK(*it == 1);
++it;
CHECK(*it == 2);
++it;
CHECK(*it == 3);
++it;
CHECK(it == set.end());
}
SUBCASE("Front and back") {
set.insert(1);
set.insert(2);
set.insert(3);
CHECK(set.front() == 1);
CHECK(set.back() == 3);
}
}

View file

@ -0,0 +1,52 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fx/frame.h"
#include <cstdlib>
#include "fl/allocator.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
namespace {
int allocation_count = 0;
void* custom_malloc(size_t size) {
allocation_count++;
return std::malloc(size);
}
void custom_free(void* ptr) {
allocation_count--;
std::free(ptr);
}
}
TEST_CASE("test frame custom allocator") {
// Set our custom allocator
SetPSRamAllocator(custom_malloc, custom_free);
FramePtr frame = FramePtr::New(100); // 100 pixels.
CHECK(allocation_count == 1); // One for RGB.
frame.reset();
// Frame should be destroyed here
CHECK(allocation_count == 0);
}
TEST_CASE("test blend by black") {
SetPSRamAllocator(custom_malloc, custom_free);
FramePtr frame = FramePtr::New(1); // 1 pixels.
frame->rgb()[0] = CRGB(255, 0, 0); // Red
CRGB out;
frame->draw(&out, DRAW_MODE_BLEND_BY_MAX_BRIGHTNESS);
CHECK(out == CRGB(255, 0, 0)); // full red because max luma is 255
out = CRGB(0, 0, 0);
frame->rgb()[0] = CRGB(128, 0, 0); // Red
frame->draw(&out, DRAW_MODE_BLEND_BY_MAX_BRIGHTNESS);
CHECK(out == CRGB(64, 0, 0));
}

View file

@ -0,0 +1,27 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fx/frame.h"
#include "fx/video/frame_tracker.h"
#include "fl/namespace.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
TEST_CASE("FrameTracker basic frame advancement") {
FrameTracker tracker(1.0f); // 1fps == 1000ms per frame
uint32_t currentFrame, nextFrame;
uint8_t amountOfNextFrame;
//tracker.reset(1000); // start at time 1000ms
// Shift the time to 500 ms or 50% of the first frame
tracker.get_interval_frames(500, &currentFrame, &nextFrame, &amountOfNextFrame);
CHECK(currentFrame == 0);
CHECK(nextFrame == 1);
CHECK(amountOfNextFrame == 127);
}

View file

@ -0,0 +1,95 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "fl/function.h"
#include "fl/function_list.h"
using namespace fl;
// Free function for testing
static int add(int a, int b) {
return a + b;
}
struct Foo {
int value = 0;
void set(int v) { value = v; }
int get() const { return value; }
};
struct Mult {
int operator()(int a, int b) const { return a * b; }
};
TEST_CASE("function<bool()> is empty by default and bool-convertible") {
function<void()> f;
REQUIRE(!f);
}
TEST_CASE("Test function with lambda") {
function<int(int,int)> f = [](int a, int b) { return a + b; };
REQUIRE(f);
REQUIRE(f(2, 3) == 5);
}
TEST_CASE("Test function with free function pointer") {
function<int(int,int)> f(add);
REQUIRE(f);
REQUIRE(f(4, 6) == 10);
}
TEST_CASE("Test function with functor object") {
Mult m;
function<int(int,int)> f(m);
REQUIRE(f);
REQUIRE(f(3, 7) == 21);
}
TEST_CASE("Test function with non-const member function") {
Foo foo;
function<void(int)> fset(&Foo::set, &foo);
REQUIRE(fset);
fset(42);
REQUIRE(foo.value == 42);
}
TEST_CASE("Test function with const member function") {
Foo foo;
foo.value = 99;
function<int()> fget(&Foo::get, &foo);
REQUIRE(fget);
REQUIRE(fget() == 99);
}
TEST_CASE("Void free function test") {
function<void(float)> f = [](float) { /* do nothing */ };
REQUIRE(f);
f(1);
}
TEST_CASE("Copy and move semantics") {
function<int(int,int)> orig = [](int a, int b) { return a - b; };
REQUIRE(orig(10, 4) == 6);
// Copy
function<int(int,int)> copy = orig;
REQUIRE(copy);
REQUIRE(copy(8, 3) == 5);
// Move
function<int(int,int)> moved = std::move(orig);
REQUIRE(moved);
REQUIRE(moved(7, 2) == 5);
REQUIRE(!orig);
}
TEST_CASE("Function list void float") {
FunctionList<float> fl;
fl.add([](float) { /* do nothing */ });
fl.invoke(1);
}

View file

@ -0,0 +1,32 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include <stdint.h>
#include "test.h"
#include "fx/1d/cylon.h"
#include "fx/1d/demoreel100.h"
#include "fx/1d/noisewave.h"
#include "fx/1d/pacifica.h"
#include "fx/1d/pride2015.h" // needs XY defined or linker error.
#include "fx/1d/twinklefox.h"
#include "fx/2d/animartrix.hpp"
#include "fx/2d/noisepalette.h"
#include "fx/2d/scale_up.h"
#include "fx/2d/redsquare.h"
#include "fx/video.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
uint16_t XY(uint8_t, uint8_t); // declaration to fix compiler warning.
// To satisfy the linker, we must also define uint16_t XY( uint8_t, uint8_t);
// This should go away someday and only use functions supplied by the user.
uint16_t XY(uint8_t, uint8_t) { return 0; }
TEST_CASE("Compile Test") {
// Suceessful compilation test
}

View file

@ -0,0 +1,186 @@
// g++ --std=c++11 test.cpp
#include <iostream>
#include "test.h"
#include "fx/2d/blend.h"
#include "fx/fx.h"
#include "fx/time.h"
#include "test.h"
#include "fl/namespace.h"
using namespace fl;
using std::cout;
// Simple test effect that fills with a solid color
class SolidColorFx2d : public fl::Fx2d {
public:
SolidColorFx2d(uint16_t width, uint16_t height, CRGB color)
: fl::Fx2d(fl::XYMap::constructRectangularGrid(width, height)),
mColor(color) {}
fl::Str fxName() const override { return "SolidColorFx2d"; }
void draw(Fx::DrawContext context) override {
for (uint16_t i = 0; i < mXyMap.getTotal(); i++) {
context.leds[i] = mColor;
}
}
private:
CRGB mColor;
};
class TestFx2D : public fl::Fx2d {
public:
TestFx2D(uint16_t width, uint16_t height)
: fl::Fx2d(fl::XYMap::constructRectangularGrid(width, height)) {
mLeds.reset(new CRGB[width * height]);
}
void set(uint16_t x, uint16_t y, CRGB color) {
if (x < mXyMap.getWidth() && y < mXyMap.getHeight()) {
uint16_t index = mXyMap(x, y);
if (index < mXyMap.getTotal()) {
mLeds[index] = color;
}
}
}
fl::Str fxName() const override { return "TestFx2D"; }
void draw(Fx::DrawContext context) override {
for (uint16_t i = 0; i < mXyMap.getTotal(); i++) {
context.leds[i] = mLeds[i];
}
}
scoped_array<CRGB> mLeds;
};
TEST_CASE("Test FX2d Layered Blending") {
const uint16_t width = 1;
const uint16_t height = 1;
XYMap xyMap = XYMap::constructRectangularGrid(width, height);
// Create a red layer
SolidColorFx2d redLayer(width, height, CRGB(255, 0, 0));
// Create a layered effect with just the red layer
fl::Blend2d blendFx(xyMap);
blendFx.add(redLayer);
// Create a buffer for the output
CRGB led;
// Draw the layered effect
Fx::DrawContext context(0, &led);
context.now = 0;
blendFx.draw(context);
// Verify the result - should be red
CHECK(led.r == 255);
CHECK(led.g == 0);
CHECK(led.b == 0);
}
TEST_CASE("Test FX2d Layered with XYMap") {
enum {
width = 2,
height = 2,
};
XYMap xyMapSerp = XYMap::constructSerpentine(width, height);
XYMap xyRect = XYMap::constructRectangularGrid(width, height);
SUBCASE("Rectangular Grid") {
// Create a blue layer
// SolidColorFx2d blueLayer(width, height, CRGB(0, 0, 255));
TestFx2D testFx(width, height);
testFx.set(0, 0, CRGB(0, 0, 255)); // Set the first pixel to blue
testFx.set(1, 0, CRGB(255, 0, 0)); // Set the second pixel to red
testFx.set(0, 1, CRGB(0, 255, 0)); // Set the third pixel to gree
testFx.set(1, 1, CRGB(0, 0, 0)); // Set the fourth pixel to black
// Create a layered effect with just the blue layer
fl::Blend2d blendFx(xyRect);
blendFx.add(testFx);
// Create a buffer for the output
CRGB led[width * height] = {};
// Draw the layered effect
Fx::DrawContext context(0, led);
context.now = 0;
blendFx.draw(context);
cout << "Layered Effect Output: " << led[0].toString().c_str() << std::endl;
cout << "Layered Effect Output: " << led[1].toString().c_str() << std::endl;
cout << "Layered Effect Output: " << led[2].toString().c_str() << std::endl;
cout << "Layered Effect Output: " << led[3].toString().c_str() << std::endl;
// Verify the result - should be blue
CHECK(led[0].r == 0);
CHECK(led[0].g == 0);
CHECK(led[0].b == 255);
CHECK(led[1].r == 255);
CHECK(led[1].g == 0);
CHECK(led[1].b == 0);
CHECK(led[2].r == 0);
CHECK(led[2].g == 255);
CHECK(led[2].b == 0);
CHECK(led[3].r == 0);
CHECK(led[3].g == 0);
CHECK(led[3].b == 0);
}
SUBCASE("Serpentine") {
// Create a blue layer
TestFx2D testFx(width, height);
testFx.set(0, 0, CRGB(0, 0, 255)); // Set the first pixel to blue
testFx.set(1, 0, CRGB(255, 0, 0)); // Set the second pixel to red
testFx.set(0, 1, CRGB(0, 255, 0)); // Set the third pixel to gree
testFx.set(1, 1, CRGB(0, 0, 0)); // Set the fourth pixel to black
// Create a layered effect with just the blue layer
fl::Blend2d blendFx(xyMapSerp);
blendFx.add(testFx);
// Create a buffer for the output
CRGB led[width * height] = {};
// Draw the layered effect
Fx::DrawContext context(0, led);
context.now = 0;
blendFx.draw(context);
cout << "Layered Effect Output: " << led[0].toString().c_str() << std::endl;
cout << "Layered Effect Output: " << led[1].toString().c_str() << std::endl;
cout << "Layered Effect Output: " << led[2].toString().c_str() << std::endl;
cout << "Layered Effect Output: " << led[3].toString().c_str() << std::endl;
// Verify the result - should be blue
CHECK(led[0].r == 0);
CHECK(led[0].g == 0);
CHECK(led[0].b == 255);
CHECK(led[1].r == 255);
CHECK(led[1].g == 0);
CHECK(led[1].b == 0);
// Now it's supposed to go up to the next line at the same column.
CHECK(led[2].r == 0);
CHECK(led[2].g == 0);
CHECK(led[2].b == 0);
CHECK(led[3].r == 0);
CHECK(led[3].g == 255);
CHECK(led[3].b == 0);
}
}

View file

@ -0,0 +1,210 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fx/fx.h"
#include "fx/fx_engine.h"
#include "fx/fx2d.h"
#include "fl/vector.h"
#include "FastLED.h"
using namespace fl;
FASTLED_SMART_PTR(MockFx);
class MockFx : public Fx {
public:
MockFx(uint16_t numLeds, CRGB color) : Fx(numLeds), mColor(color) {}
void draw(DrawContext ctx) override {
mLastDrawTime = ctx.now;
for (uint16_t i = 0; i < mNumLeds; ++i) {
ctx.leds[i] = mColor;
}
}
Str fxName() const override { return "MockFx"; }
private:
CRGB mColor;
uint32_t mLastDrawTime = 0;
};
TEST_CASE("test_fx_engine") {
constexpr uint16_t NUM_LEDS = 10;
FxEngine engine(NUM_LEDS, false);
CRGB leds[NUM_LEDS];
Ptr<MockFx> redFx = MockFxPtr::New(NUM_LEDS, CRGB::Red);
Ptr<MockFx> blueFx = MockFxPtr::New(NUM_LEDS, CRGB::Blue);
int id0 = engine.addFx(redFx);
int id1 = engine.addFx(blueFx);
REQUIRE_EQ(0, id0);
REQUIRE_EQ(1, id1);
SUBCASE("Initial state") {
int currId = engine.getCurrentFxId();
CHECK(currId == id0);
const bool ok = engine.draw(0, leds);
CHECK(ok);
for (uint16_t i = 0; i < NUM_LEDS; ++i) {
// CHECK(leds[i] == CRGB::Red);
bool is_red = leds[i] == CRGB::Red;
if (!is_red) {
Str err = leds[i].toString();
printf("leds[%d] is not red, was instead: %s\n", i, err.c_str());
CHECK(is_red);
}
}
}
SUBCASE("Transition") {
bool ok = engine.nextFx(1000);
if (!ok) {
auto& effects = engine._getEffects();
for (auto it = effects.begin(); it != effects.end(); ++it) {
auto& fx = it->second;
printf("fx: %s\n", fx->fxName().c_str());
}
FAIL("Failed to transition to next effect");
}
REQUIRE(ok);
// Start of transition
ok = engine.draw(0, leds);
REQUIRE(ok);
for (uint16_t i = 0; i < NUM_LEDS; ++i) {
REQUIRE(leds[i] == CRGB::Red);
}
// Middle of transition
REQUIRE(engine.draw(500, leds));
for (uint16_t i = 0; i < NUM_LEDS; ++i) {
REQUIRE(leds[i].r == 128);
REQUIRE(leds[i].g == 0);
REQUIRE(leds[i].b == 127);
}
// End of transition
REQUIRE(engine.draw(1000, leds));
for (uint16_t i = 0; i < NUM_LEDS; ++i) {
CHECK(leds[i] == CRGB::Blue);
}
}
SUBCASE("Transition with 0 time duration") {
engine.nextFx(0);
engine.draw(0, leds);
for (uint16_t i = 0; i < NUM_LEDS; ++i) {
CHECK(leds[i] == CRGB::Blue);
}
}
}
TEST_CASE("test_transition") {
SUBCASE("Initial state") {
Transition transition;
CHECK(transition.getProgress(0) == 0);
CHECK_FALSE(transition.isTransitioning(0));
}
SUBCASE("Start transition") {
Transition transition;
transition.start(100, 1000);
CHECK(transition.isTransitioning(100));
CHECK(transition.isTransitioning(1099));
CHECK_FALSE(transition.isTransitioning(1100));
}
SUBCASE("Progress calculation") {
Transition transition;
transition.start(100, 1000);
CHECK(transition.getProgress(100) == 0);
CHECK(transition.getProgress(600) == 127);
CHECK(transition.getProgress(1100) == 255);
}
SUBCASE("Progress before start time") {
Transition transition;
transition.start(100, 1000);
CHECK(transition.getProgress(50) == 0);
}
SUBCASE("Progress after end time") {
Transition transition;
transition.start(100, 1000);
CHECK(transition.getProgress(1200) == 255);
}
SUBCASE("Multiple transitions") {
Transition transition;
transition.start(100, 1000);
CHECK(transition.isTransitioning(600));
transition.start(2000, 500);
CHECK_FALSE(transition.isTransitioning(1500));
CHECK(transition.isTransitioning(2200));
CHECK(transition.getProgress(2250) == 127);
}
SUBCASE("Zero duration transition") {
Transition transition;
transition.start(100, 0);
CHECK_FALSE(transition.isTransitioning(100));
CHECK(transition.getProgress(99) == 0);
CHECK(transition.getProgress(100) == 255);
CHECK(transition.getProgress(101) == 255);
}
}
// Simple Fx2d object which writes a single red pixel to the first LED
// with the red component being the intensity of the frame counter.
class Fake2d : public Fx2d {
public:
Fake2d() : Fx2d(XYMap::constructRectangularGrid(1,1)) {}
void draw(DrawContext context) override {
CRGB c = mColors[mFrameCounter % mColors.size()];
context.leds[0] = c;
mFrameCounter++;
}
bool hasFixedFrameRate(float *fps) const override {
*fps = 1;
return true;
}
Str fxName() const override { return "Fake2d"; }
uint8_t mFrameCounter = 0;
FixedVector<CRGB, 5> mColors;
};
TEST_CASE("test_fixed_fps") {
Fake2d fake;
fake.mColors.push_back(CRGB(0, 0, 0));
fake.mColors.push_back(CRGB(255, 0, 0));
CRGB leds[1];
bool interpolate = true;
FxEngine engine(1, interpolate);
int id = engine.addFx(fake);
CHECK_EQ(0, id);
engine.draw(0, &leds[0]);
CHECK_EQ(1, fake.mFrameCounter);
CHECK_EQ(leds[0], CRGB(0, 0, 0));
engine.draw(500, &leds[0]);
CHECK_EQ(2, fake.mFrameCounter);
CHECK_EQ(leds[0], CRGB(127, 0, 0));
}

View file

@ -0,0 +1,60 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fx/time.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
TEST_CASE("TimeWarp basic functionality") {
FASTLED_USING_NAMESPACE;
SUBCASE("Initialization and normal time progression") {
TimeWarp tw(1000, 1.0f); // 1000 ms is the start time, speed is set at 1x
CHECK(tw.time() == 0);
CHECK(tw.scale() == 1.0f);
tw.update(2000);
CHECK(tw.time() == 1000);
}
SUBCASE("Time scaling") {
TimeWarp tw(1000);
tw.setSpeed(2.0f); // now we are at 2x speed.
CHECK(tw.time() == 0); // at t0 = 1000ms
tw.update(1500); // we give 500 at 2x => add 1000 to time counter.
CHECK(tw.time() == 1000);
tw.setSpeed(0.5f); // Set half speed: 500ms.
CHECK(tw.scale() == 0.5f);
tw.update(2500);
CHECK(tw.time() == 1500);
}
SUBCASE("Reset functionality") {
TimeWarp tw(1000, 1.0f);
tw.update(2000);
CHECK(tw.time() == 1000);
tw.reset(3000);
CHECK(tw.time() == 0);
tw.update(4000);
CHECK(tw.time() == 1000);
}
SUBCASE("Wrap-around protection - prevent from going below start time") {
TimeWarp tw(1000, 1.0f);
tw.update(1001);
CHECK(tw.time() == 1);
tw.setSpeed(-1.0f);
tw.update(2000);
CHECK_EQ(tw.time(), 0);
}
}

View file

@ -0,0 +1,24 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/grid.h"
using namespace fl;
TEST_CASE("Grid_int16_t") {
Grid<int16_t> grid(2, 2);
REQUIRE_EQ(grid.width(), 2);
REQUIRE_EQ(grid.height(), 2);
auto min_max = grid.minMax();
REQUIRE_EQ(min_max.x, 0);
REQUIRE_EQ(min_max.y, 0);
grid.at(0, 0) = 32767;
REQUIRE_EQ(32767, grid.at(0, 0));
}

View file

@ -0,0 +1,300 @@
// test.cpp
// g++ --std=c++11 test.cpp -o test && ./test
#include <vector>
#include <set>
#include <unordered_map>
#include "fl/hash_map.h"
#include "fl/str.h"
#include "test.h"
using namespace fl;
TEST_CASE("Empty map properties") {
HashMap<int, int> m;
REQUIRE_EQ(m.size(), 0u);
REQUIRE(!m.find_value(42));
// begin()==end() on empty
REQUIRE(m.begin() == m.end());
}
TEST_CASE("Single insert, lookup & operator[]") {
HashMap<int, int> m;
m.insert(10, 20);
REQUIRE_EQ(m.size(), 1u);
auto *v = m.find_value(10);
REQUIRE(v);
REQUIRE_EQ(*v, 20);
// operator[] default-construct & assignment
HashMap<int, Str> ms;
auto &ref = ms[5];
REQUIRE(ref.empty()); // default-constructed
REQUIRE_EQ(ms.size(), 1u);
ref = "hello";
REQUIRE_EQ(*ms.find_value(5), "hello");
// operator[] overwrite existing
ms[5] = "world";
REQUIRE_EQ(ms.size(), 1u);
REQUIRE_EQ(*ms.find_value(5), "world");
}
TEST_CASE("Insert duplicate key overwrites without growing") {
HashMap<int, Str> m;
m.insert(1, "foo");
REQUIRE_EQ(m.size(), 1u);
REQUIRE_EQ(*m.find_value(1), "foo");
m.insert(1, "bar");
REQUIRE_EQ(m.size(), 1u);
REQUIRE_EQ(*m.find_value(1), "bar");
}
TEST_CASE("Multiple distinct inserts & lookups") {
HashMap<char, int> m;
int count = 0;
for (char c = 'a'; c < 'a' + 10; ++c) {
MESSAGE("insert " << count++);
m.insert(c, c - 'a');
}
REQUIRE_EQ(m.size(), 10u);
for (char c = 'a'; c < 'a' + 10; ++c) {
auto *v = m.find_value(c);
REQUIRE(v);
REQUIRE_EQ(*v, static_cast<int>(c - 'a'));
}
REQUIRE(!m.find_value('z'));
}
TEST_CASE("Erase and remove behavior") {
HashMap<int, int> m;
m.insert(5, 55);
m.insert(6, 66);
REQUIRE_EQ(m.size(), 2u);
// erase existing
REQUIRE(m.erase(5));
REQUIRE_EQ(m.size(), 1u);
REQUIRE(!m.find_value(5));
// erase non-existent
REQUIRE(!m.erase(5));
REQUIRE_EQ(m.size(), 1u);
REQUIRE(m.erase(6));
REQUIRE_EQ(m.size(), 0u);
}
TEST_CASE("Re-insert after erase reuses slot") {
HashMap<int, int> m(4);
m.insert(1, 10);
REQUIRE(m.erase(1));
REQUIRE(!m.find_value(1));
REQUIRE_EQ(m.size(), 0u);
m.insert(1, 20);
REQUIRE(m.find_value(1));
REQUIRE_EQ(*m.find_value(1), 20);
REQUIRE_EQ(m.size(), 1u);
}
TEST_CASE("Clear resets map and allows fresh inserts") {
HashMap<int, int> m(4);
for (int i = 0; i < 3; ++i)
m.insert(i, i);
m.remove(1);
REQUIRE_EQ(m.size(), 2u);
m.clear();
REQUIRE_EQ(m.size(), 0u);
REQUIRE(!m.find_value(0));
REQUIRE(!m.find_value(1));
REQUIRE(!m.find_value(2));
m.insert(5, 50);
REQUIRE_EQ(m.size(), 1u);
REQUIRE_EQ(*m.find_value(5), 50);
}
TEST_CASE("Stress collisions & rehash with small initial capacity") {
HashMap<int, int> m(1 /*capacity*/);
const int N = 100;
for (int i = 0; i < N; ++i) {
m.insert(i, i * 3);
// test that size is increasing
REQUIRE_EQ(m.size(), static_cast<std::size_t>(i + 1));
}
REQUIRE_EQ(m.size(), static_cast<std::size_t>(N));
for (int i = 0; i < N; ++i) {
auto *v = m.find_value(i);
REQUIRE(v);
REQUIRE_EQ(*v, i * 3);
}
}
TEST_CASE("Iterator round-trip and const-iteration") {
HashMap<int, int> m;
for (int i = 0; i < 20; ++i) {
m.insert(i, i + 100);
}
// non-const iteration
std::size_t count = 0;
for (auto kv : m) {
REQUIRE_EQ(kv.second, kv.first + 100);
++count;
}
REQUIRE_EQ(count, m.size());
// const iteration
const auto &cm = m;
count = 0;
for (auto it = cm.begin(); it != cm.end(); ++it) {
auto kv = *it;
REQUIRE_EQ(kv.second, kv.first + 100);
++count;
}
REQUIRE_EQ(count, cm.size());
}
TEST_CASE("Remove non-existent returns false, find on const map") {
HashMap<int, int> m;
REQUIRE(!m.remove(999));
const HashMap<int, int> cm;
REQUIRE(!cm.find_value(0));
}
TEST_CASE("Inserting multiple elements while deleting them will trigger inline "
"rehash") {
const static int MAX_CAPACITY = 2;
HashMap<int, int> m(8 /*capacity*/);
REQUIRE_EQ(8, m.capacity());
for (int i = 0; i < 8; ++i) {
m.insert(i, i);
if (m.size() > MAX_CAPACITY) {
m.remove(i);
}
}
size_t new_capacity = m.capacity();
// should still be 8
REQUIRE_EQ(new_capacity, 8u);
std::set<int> found_values;
for (auto it = m.begin(); it != m.end(); ++it) {
auto kv = *it;
auto key = kv.first;
auto value = kv.second;
REQUIRE_EQ(key, value);
found_values.insert(kv.second);
}
std::vector<int> found_values_vec(found_values.begin(), found_values.end());
REQUIRE_EQ(MAX_CAPACITY, found_values_vec.size());
for (int i = 0; i < MAX_CAPACITY; ++i) {
auto value = found_values_vec[i];
REQUIRE_EQ(value, i);
}
}
TEST_CASE("HashMap with standard iterator access") {
HashMap<int, int> m;
m.insert(1, 1);
REQUIRE_EQ(m.size(), 1u);
// standard iterator access
auto it = m.begin();
auto entry = *it;
REQUIRE_EQ(entry.first, 1);
REQUIRE_EQ(entry.second, 1);
++it;
REQUIRE(it == m.end());
auto bad_it = m.find(0);
REQUIRE(bad_it == m.end());
}
TEST_CASE("HashMap equivalence to std::unordered_map") {
// Create both map types with the same operations
HashMap<int, fl::Str> custom_map;
std::unordered_map<int, fl::Str> std_map;
// Test insertion
custom_map.insert(1, "one");
std_map.insert({1, "one"});
custom_map.insert(2, "two");
std_map.insert({2, "two"});
custom_map.insert(3, "three");
std_map.insert({3, "three"});
// Test size
REQUIRE_EQ(custom_map.size(), std_map.size());
// Test lookup
REQUIRE(*custom_map.find_value(1) == std_map.at(1));
REQUIRE(*custom_map.find_value(2) == std_map.at(2));
REQUIRE(*custom_map.find_value(3) == std_map.at(3));
// Test non-existent key
REQUIRE(!custom_map.find_value(99));
bool std_throws = false;
try {
std_map.at(99);
} catch (const std::out_of_range&) {
std_throws = true;
}
REQUIRE(std_throws);
// Test overwrite
custom_map.insert(2, "TWO");
std_map[2] = "TWO";
REQUIRE(*custom_map.find_value(2) == std_map.at(2));
// Test erase
REQUIRE(custom_map.erase(2));
std_map.erase(2);
REQUIRE_EQ(custom_map.size(), std_map.size());
REQUIRE(!custom_map.find_value(2));
// Test clear
custom_map.clear();
std_map.clear();
REQUIRE_EQ(custom_map.size(), std_map.size());
REQUIRE_EQ(custom_map.size(), 0u);
// Test operator[]
custom_map[5] = "five";
std_map[5] = "five";
REQUIRE_EQ(custom_map.size(), std_map.size());
REQUIRE(*custom_map.find_value(5) == std_map.at(5));
// Test iteration (collect all keys and values)
for (int i = 10; i < 20; ++i) {
fl::Str val = "val";
val.append(i);
custom_map.insert(i, val);
std_map.insert({i, val});
}
std::set<int> custom_keys;
std::set<fl::Str> custom_values;
for (auto kv : custom_map) {
custom_keys.insert(kv.first);
custom_values.insert(kv.second);
}
std::set<int> std_keys;
std::set<fl::Str> std_values;
for (auto& kv : std_map) {
std_keys.insert(kv.first);
std_values.insert(kv.second);
}
REQUIRE(custom_keys == std_keys);
REQUIRE(custom_values == std_values);
}

View file

@ -0,0 +1,151 @@
// test.cpp
// g++ --std=c++11 test.cpp -o test && ./test
#include <vector>
#include <set>
#include <unordered_map>
#include "fl/hash_map_lru.h"
#include "fl/str.h"
#include "test.h"
using namespace fl;
TEST_CASE("Test HashMapLRU") {
SUBCASE("Basic operations") {
HashMapLru<int, int> lru(3);
// Test empty state
CHECK(lru.empty());
CHECK(lru.size() == 0);
CHECK(lru.capacity() == 3);
CHECK(lru.find_value(1) == nullptr);
// Test insertion
lru.insert(1, 100);
CHECK(!lru.empty());
CHECK(lru.size() == 1);
CHECK(*lru.find_value(1) == 100);
// Test operator[]
lru[2] = 200;
CHECK(lru.size() == 2);
CHECK(*lru.find_value(2) == 200);
// Test update
lru[1] = 150;
CHECK(lru.size() == 2);
CHECK(*lru.find_value(1) == 150);
// Test removal
CHECK(lru.remove(1));
CHECK(lru.size() == 1);
CHECK(lru.find_value(1) == nullptr);
CHECK(!lru.remove(1)); // Already removed
// Test clear
lru.clear();
CHECK(lru.empty());
CHECK(lru.size() == 0);
}
SUBCASE("LRU eviction") {
HashMapLru<int, int> lru(3);
// Fill the cache
lru.insert(1, 100);
lru.insert(2, 200);
lru.insert(3, 300);
CHECK(lru.size() == 3);
// Access key 1 to make it most recently used
CHECK(*lru.find_value(1) == 100);
// Insert a new key, should evict key 2 (least recently used)
lru.insert(4, 400);
CHECK(lru.size() == 3);
CHECK(lru.find_value(2) == nullptr); // Key 2 should be evicted
CHECK(*lru.find_value(1) == 100);
CHECK(*lru.find_value(3) == 300);
CHECK(*lru.find_value(4) == 400);
// Access key 3, then insert new key
CHECK(*lru.find_value(3) == 300);
lru.insert(5, 500);
CHECK(lru.size() == 3);
CHECK(lru.find_value(1) == nullptr); // Key 1 should be evicted
CHECK(*lru.find_value(3) == 300);
CHECK(*lru.find_value(4) == 400);
CHECK(*lru.find_value(5) == 500);
}
SUBCASE("Operator[] LRU behavior") {
HashMapLru<int, int> lru(3);
// Fill the cache using operator[]
lru[1] = 100;
lru[2] = 200;
lru[3] = 300;
// Access key 1 to make it most recently used
int val = lru[1];
CHECK(val == 100);
// Insert a new key, should evict key 2
lru[4] = 400;
CHECK(lru.size() == 3);
CHECK(lru.find_value(2) == nullptr);
CHECK(*lru.find_value(1) == 100);
CHECK(*lru.find_value(3) == 300);
CHECK(*lru.find_value(4) == 400);
}
SUBCASE("Edge cases") {
// Test with capacity 1
HashMapLru<int, int> tiny_lru(1);
tiny_lru.insert(1, 100);
CHECK(*tiny_lru.find_value(1) == 100);
tiny_lru.insert(2, 200);
CHECK(tiny_lru.size() == 1);
CHECK(tiny_lru.find_value(1) == nullptr);
CHECK(*tiny_lru.find_value(2) == 200);
// Test with string keys
HashMapLru<fl::Str, int> str_lru(2);
str_lru.insert("one", 1);
str_lru.insert("two", 2);
CHECK(*str_lru.find_value("one") == 1);
CHECK(*str_lru.find_value("two") == 2);
// This should evict "one" since it's least recently used
str_lru.insert("three", 3);
CHECK(str_lru.find_value("one") == nullptr);
CHECK(*str_lru.find_value("two") == 2);
CHECK(*str_lru.find_value("three") == 3);
}
SUBCASE("Update existing key") {
HashMapLru<int, int> lru(3);
// Fill the cache
lru.insert(1, 100);
lru.insert(2, 200);
lru.insert(3, 300);
// Update an existing key
lru.insert(2, 250);
CHECK(lru.size() == 3);
CHECK(*lru.find_value(1) == 100);
CHECK(*lru.find_value(2) == 250);
CHECK(*lru.find_value(3) == 300);
// Insert a new key, should evict key 1 (least recently used)
lru.insert(4, 400);
CHECK(lru.size() == 3);
CHECK(lru.find_value(1) == nullptr);
CHECK(*lru.find_value(2) == 250);
CHECK(*lru.find_value(3) == 300);
CHECK(*lru.find_value(4) == 400);
}
}

View file

@ -0,0 +1,22 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "lib8tion/intmap.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
TEST_CASE("map8_to_16") {
CHECK_EQ(map8_to_16(0), 0);
CHECK_EQ(map8_to_16(1), 0x101);
CHECK_EQ(map8_to_16(0xff), 0xffff);
}
TEST_CASE("map8_to_32") {
CHECK_EQ(map8_to_32(0), 0);
CHECK_EQ(map8_to_32(1), 0x1010101);
CHECK_EQ(map8_to_32(0xff), 0xffffffff);
}

View file

@ -0,0 +1,207 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include <random>
#include "fl/line_simplification.h"
#include "fl/warn.h"
using namespace fl;
TEST_CASE("Test Line Simplification") {
// defaultconstructed bitset is empty
LineSimplifier<float> ls;
ls.setMinimumDistance(0.1f);
fl::vector<vec2<float>> points;
points.push_back({0.0f, 0.0f});
points.push_back({1.0f, 1.0f});
points.push_back({2.0f, 2.0f});
points.push_back({3.0f, 3.0f});
points.push_back({4.0f, 4.0f});
ls.simplifyInplace(&points);
REQUIRE_EQ(2,
points.size()); // Only 2 points on co-linear line should remain.
REQUIRE_EQ(vec2<float>(0.0f, 0.0f), points[0]);
REQUIRE_EQ(vec2<float>(4.0f, 4.0f), points[1]);
}
TEST_CASE("Test simple triangle") {
LineSimplifier<float> ls;
fl::vector<vec2<float>> points;
points.push_back({0.0f, 0.0f}); // First point of triangle
points.push_back({0.5f, 0.5f});
points.push_back({0.0f, 1.0f});
float exceeds_thresh = 0.49f;
float under_thresh = 0.51f;
ls.setMinimumDistance(exceeds_thresh);
fl::vector<vec2<float>> output;
ls.simplify(points, &output);
REQUIRE_EQ(3, output.size());
REQUIRE_EQ(vec2<float>(0.0f, 0.0f), output[0]);
REQUIRE_EQ(vec2<float>(0.5f, 0.5f), output[1]);
REQUIRE_EQ(vec2<float>(0.0f, 1.0f), output[2]);
ls.setMinimumDistance(under_thresh);
ls.simplify(points, &output);
REQUIRE_EQ(2, output.size());
REQUIRE_EQ(vec2<float>(0.0f, 0.0f), output[0]);
REQUIRE_EQ(vec2<float>(0.0f, 1.0f), output[1]);
}
TEST_CASE("Test Line Simplification with Different Distance Thresholds") {
LineSimplifier<float> ls;
// Test with a triangle shape - non-collinear points
ls.setMinimumDistance(0.5f);
fl::vector<vec2<float>> points1;
points1.push_back({0.0f, 0.0f}); // First point of triangle
points1.push_back({0.3f, 0.3f}); // Should be filtered out (distance < 0.5)
points1.push_back({1.0f, 1.0f}); // Second point of triangle
points1.push_back({0.8f, 1.2f}); // Should be filtered out (distance < 0.5)
points1.push_back({0.0f, 2.0f}); // Third point of triangle
ls.simplifyInplace(&points1);
REQUIRE_EQ(3, points1.size()); // Triangle vertices should remain
REQUIRE_EQ(vec2<float>(0.0f, 0.0f), points1[0]);
REQUIRE_EQ(vec2<float>(1.0f, 1.0f), points1[1]);
REQUIRE_EQ(vec2<float>(0.0f, 2.0f), points1[2]);
}
TEST_CASE("Test Line Simplification with Complex Shape") {
// SUBCASE("at threshold") {
// LineSimplifier<float> ls;
// // Test with a more complex shape and smaller threshold
// ls.setMinimumDistance(0.1f);
// fl::vector<vec2<float>> points2;
// points2.push_back({0.0f, 0.0f}); // Start point
// points2.push_back({0.1f, 0.20f}); // Filtered out
// points2.push_back({0.0f, 0.29f}); // Filtered out
// points2.push_back({0.0f, 1.0f}); // Should be kept (distance > 0.2)
// ls.simplifyInplace(&points2);
// REQUIRE_EQ(3, points2.size());
// REQUIRE_EQ(vec2<float>(0.0f, 0.0f), points2[0]);
// REQUIRE_EQ(vec2<float>(0.10f, 0.10f), points2[1]);
// REQUIRE_EQ(vec2<float>(0.0f, 1.0f), points2[2]);
// };
SUBCASE("Above threshold") {
LineSimplifier<float> ls;
// Test with a more complex shape and larger threshold
ls.setMinimumDistance(0.101f);
fl::vector<vec2<float>> points3;
points3.push_back({0.0f, 0.0f}); // Start point
points3.push_back({0.1f, 0.1f}); // Filtered out
points3.push_back({0.0f, 0.3f}); // Filtered out
points3.push_back({0.0f, 1.0f}); // Should be kept (distance > 0.5)
ls.simplifyInplace(&points3);
REQUIRE_EQ(2, points3.size());
REQUIRE_EQ(vec2<float>(0.0f, 0.0f), points3[0]);
REQUIRE_EQ(vec2<float>(0.0f, 1.0f), points3[1]);
};
}
TEST_CASE("Iteratively find the closest point") {
LineSimplifier<float> ls;
fl::vector<vec2<float>> points;
points.push_back({0.0f, 0.0f}); // First point of triangle
points.push_back({0.5f, 0.5f});
points.push_back({0.0f, 1.0f});
float thresh = 0.0;
while (true) {
ls.setMinimumDistance(thresh);
fl::vector<vec2<float>> output;
ls.simplify(points, &output);
if (output.size() == 2) {
break;
}
thresh += 0.01f;
}
REQUIRE(ALMOST_EQUAL(thresh, 0.5f, 0.01f));
}
TEST_CASE("Binary search the the threshold that gives 3 points") {
LineSimplifierExact<float> ls;
fl::vector<vec2<float>> points;
points.push_back({0.0f, 0.0f}); // First point of triangle
points.push_back({0.5f, 0.5f});
points.push_back({0.0f, 1.0f});
points.push_back({0.6f, 2.0f});
points.push_back({0.0f, 6.0f});
ls.setCount(3);
fl::vector<vec2<float>> out;
ls.simplify(points, &out);
REQUIRE_EQ(3, out.size());
MESSAGE("Done");
}
TEST_CASE("Known bad") {
fl::vector<vec2<float>> points;
points.push_back({-3136.439941f, 2546.339844f});
points.push_back({4580.994141f, -3516.982422f});
points.push_back({-1228.554688f, -5104.814453f});
points.push_back({-8806.442383f, 3895.103516f});
points.push_back({-2039.114746f, 1878.047852f});
LineSimplifierExact<float> ls;
ls.setCount(3);
fl::vector<vec2<float>> out;
ls.simplify(points, &out);
MESSAGE("Output points: " << out.size());
MESSAGE("Output points: " << out);
REQUIRE_EQ(3, out.size());
}
// TEST_CASE("Binary search reduction to 3 points from 5 random points (1000 runs)") {
// constexpr int kTrials = 1000;
// constexpr int kInputPoints = 5;
// constexpr int kTargetPoints = 3;
// std::mt19937 rng(123); // fixed seed for reproducibility
// std::uniform_real_distribution<float> dist(-10000.0f, 10000.0f);
// for (int trial = 0; trial < kTrials; ++trial) {
// LineSimplifierExact<float> ls;
// fl::vector<vec2<float>> points;
// for (int i = 0; i < kInputPoints; ++i) {
// points.push_back({dist(rng), dist(rng)});
// }
// ls.setCount(kTargetPoints);
// fl::vector<vec2<float>> out;
// ls.simplify(points, &out);
// const bool bad_value = (out.size() != kTargetPoints);
// if (bad_value) {
// INFO("Trial " << trial << ": Input points: " << points.size()
// << ", Output points: " << out.size() << ", " << out);
// for (size_t i = 0; i < points.size(); ++i) {
// auto p = points[i];
// FASTLED_WARN("Input point " << i << ": " << p);
// }
// // Assert
// REQUIRE_EQ(kTargetPoints, out.size());
// }
// }
// MESSAGE("Completed 1000 trials of random 5→3 simplification");
// }

View file

@ -0,0 +1,43 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "lib8tion/intmap.h"
#include "fl/lut.h"
using namespace fl;
TEST_CASE("LUT interp8") {
LUT<uint16_t> lut(2);
uint16_t* data = lut.getDataMutable();
data[0] = 0;
data[1] = 255;
CHECK_EQ(lut.interp8(0), 0);
CHECK_EQ(lut.interp8(255), 255);
CHECK_EQ(lut.interp8(128), 128);
// Check the LUT interpolation for all values.
for (uint16_t i = 0; i < 256; i++) {
uint16_t expected = (i * 255) / 255;
CHECK_EQ(lut.interp8(i), expected);
}
}
TEST_CASE("LUT interp16") {
LUT<uint16_t> lut(2);
uint16_t* data = lut.getDataMutable();
data[0] = 0;
data[1] = 255;
CHECK_EQ(lut.interp16(0), 0);
CHECK_EQ(lut.interp16(0xffff), 255);
CHECK_EQ(lut.interp16(0xffff / 2), 127);
// Check the LUT interpolation for all values.
for (int i = 0; i < 256; i++) {
uint16_t alpha16 = map8_to_16(i);
CHECK_EQ(i, lut.interp16(alpha16));
}
}

View file

@ -0,0 +1,139 @@
// g++ --std=c++11 test_fixed_map.cpp -I../src
#include "test.h"
#include "test.h"
#include "fl/map.h"
#include <string>
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
TEST_CASE("fl::FixedMap operations") {
fl::FixedMap<int, int, 5> map;
SUBCASE("Insert and find") {
CHECK(map.insert(1, 10).first);
CHECK(map.insert(2, 20).first);
CHECK(map.insert(3, 30).first);
int value;
CHECK(map.get(1, &value));
CHECK(value == 10);
CHECK(map.get(2, &value));
CHECK(value == 20);
CHECK(map.get(3, &value));
CHECK(value == 30);
CHECK_FALSE(map.get(4, &value));
}
SUBCASE("Update") {
CHECK(map.insert(1, 10).first);
CHECK(map.update(1, 15));
int value;
CHECK(map.get(1, &value));
CHECK(value == 15);
CHECK(map.update(2, 20, true)); // Insert if missing
CHECK(map.get(2, &value));
CHECK(value == 20);
CHECK_FALSE(map.update(3, 30, false)); // Don't insert if missing
CHECK_FALSE(map.get(3, &value));
}
SUBCASE("Next and prev") {
CHECK(map.insert(1, 10).first);
CHECK(map.insert(2, 20).first);
CHECK(map.insert(3, 30).first);
int next_key;
CHECK(map.next(1, &next_key));
CHECK(next_key == 2);
CHECK(map.next(2, &next_key));
CHECK(next_key == 3);
CHECK_FALSE(map.next(3, &next_key));
CHECK(map.next(3, &next_key, true)); // With rollover
CHECK(next_key == 1);
int prev_key;
CHECK(map.prev(3, &prev_key));
CHECK(prev_key == 2);
CHECK(map.prev(2, &prev_key));
CHECK(prev_key == 1);
CHECK_FALSE(map.prev(1, &prev_key));
CHECK(map.prev(1, &prev_key, true)); // With rollover
CHECK(prev_key == 3);
// Additional test for prev on first element with rollover
CHECK(map.prev(1, &prev_key, true));
CHECK(prev_key == 3);
}
SUBCASE("Size and capacity") {
CHECK(map.size() == 0);
CHECK(map.capacity() == 5);
CHECK(map.empty());
CHECK(map.insert(1, 10).first);
CHECK(map.insert(2, 20).first);
CHECK(map.size() == 2);
CHECK_FALSE(map.empty());
map.clear();
CHECK(map.size() == 0);
CHECK(map.empty());
}
SUBCASE("Iterators") {
CHECK(map.insert(1, 10).first);
CHECK(map.insert(2, 20).first);
CHECK(map.insert(3, 30).first);
int sum = 0;
for (const auto& pair : map) {
sum += pair.second;
}
CHECK(sum == 60);
}
SUBCASE("Operator[]") {
CHECK(map[1] == 0); // Default value
CHECK(!map.insert(1, 10).first);
CHECK(map[1] == 0);
CHECK(map[2] == 0); // Default value
}
}
TEST_CASE("SortedHeapMap operations") {
struct Less {
bool operator()(int a, int b) const { return a < b; }
};
SUBCASE("Insert maintains key order") {
fl::SortedHeapMap<int, std::string, Less> map;
map.insert(3, "three");
map.insert(1, "one");
map.insert(4, "four");
map.insert(2, "two");
CHECK(map.size() == 4);
CHECK(map.has(1));
CHECK(map.has(2));
CHECK(map.has(3));
CHECK(map.has(4));
CHECK_FALSE(map.has(5));
// Verify order by iterating
auto it = map.begin();
CHECK(it->first == 1);
++it;
CHECK(it->first == 2);
++it;
CHECK(it->first == 3);
++it;
CHECK(it->first == 4);
++it;
CHECK(it == map.end());
}
}

View file

@ -0,0 +1,54 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/map_range.h"
using namespace fl;
TEST_CASE("map_range<uint8_t>") {
CHECK_EQ(map_range<uint8_t>(0, 0, 255, 0, 255), 0);
CHECK_EQ(map_range<uint8_t>(255, 0, 255, 0, 255), 255);
CHECK_EQ(map_range<uint8_t>(128, 0, 255, 0, 255), 128);
CHECK_EQ(map_range<uint8_t>(128, 0, 255, 0, 127), 63);
// One past the max should roll over to to 128.
CHECK_EQ(map_range<uint8_t>(128, 0, 127, 0, 127), 128);
}
TEST_CASE("map_range<uint16_t>") {
CHECK_EQ(map_range<uint16_t>(0, 0, 65535, 0, 65535), 0);
CHECK_EQ(map_range<uint16_t>(65535, 0, 65535, 0, 65535), 65535);
CHECK_EQ(map_range<uint16_t>(32768, 0, 65535, 0, 65535), 32768);
CHECK_EQ(map_range<uint16_t>(32768, 0, 65535, 0, 32767), 16383);
CHECK_EQ(map_range<uint16_t>(32768, 0, 32767, 0, 32767), 32768);
}
TEST_CASE("map_range<float>") {
CHECK_EQ(map_range<float>(0.0f, 0.0f, 1.0f, 0.0f, 1.0f), 0.0f);
CHECK_EQ(map_range<float>(1.0f, 0.0f, 1.0f, 0.0f, 1.0f), 1.0f);
CHECK_EQ(map_range<float>(0.5f, 0.0f, 1.0f, 0.0f, 1.0f), 0.5f);
CHECK_EQ(map_range<float>(0.5f, 0.0f, 1.0f, 10.0f, 20.0f), 15.0f);
CHECK_EQ(map_range<float>(2.5f, -1.5f, 2.5f, -10.5f, -20.5f), -20.5f);
}
TEST_CASE("map_range<float, vec2<float>") {
float min = 0.0f;
float max = 1.0f;
vec2<float> in_min(0.0f, 0.0f);
vec2<float> out_max(1.0f, 2.0f);
vec2<float> out = map_range(.5f, min, max, in_min, out_max);
CHECK_EQ(out.x, 0.5f);
CHECK_EQ(out.y, 1.f);
// Now try negative number
out = map_range(-1.f, min, max, in_min, out_max);
CHECK_EQ(out.x, -1.f);
CHECK_EQ(out.y, -2.f);
}

View file

@ -0,0 +1,77 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "FastLED.h"
#include "lib8tion/scale8.h"
#include "lib8tion/intmap.h"
#include <math.h>
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
TEST_CASE("scale16") {
CHECK_EQ(scale16(0, 0), 0);
CHECK_EQ(scale16(0, 1), 0);
CHECK_EQ(scale16(1, 0), 0);
CHECK_EQ(scale16(0xffff, 0xffff), 0xffff);
CHECK_EQ(scale16(0xffff, 0xffff >> 1), 0xffff >> 1);
CHECK_EQ(scale16(0xffff >> 1, 0xffff >> 1), 0xffff >> 2);
for (int i = 0; i < 16; ++i) {
for (int j = 0; j < 16; ++j) {
int total_bitshift = i + j;
if (total_bitshift > 15) {
break;
}
// print out info if this test fails to capture the i,j values that are failing
INFO("i: " << i << " j: " << j << " total_bitshift: " << total_bitshift);
CHECK_EQ(scale16(0xffff >> i, 0xffff >> j), 0xffff >> total_bitshift);
}
}
}
TEST_CASE("scale16by8") {
CHECK_EQ(scale16by8(0, 0), 0);
CHECK_EQ(scale16by8(0, 1), 0);
CHECK_EQ(scale16by8(1, 0), 0);
CHECK_EQ(scale16by8(map8_to_16(1), 1), 2);
CHECK_EQ(scale16by8(0xffff, 0xff), 0xffff);
CHECK_EQ(scale16by8(0xffff, 0xff >> 1), 0xffff >> 1);
CHECK_EQ(scale16by8(0xffff >> 1, 0xff >> 1), 0xffff >> 2);
for (int i = 0; i < 16; ++i) {
for (int j = 0; j < 8; ++j) {
int total_bitshift = i + j;
if (total_bitshift > 7) {
break;
}
// print out info if this test fails to capture the i,j values that are failing
INFO("i: " << i << " j: " << j << " total_bitshift: " << total_bitshift);
CHECK_EQ(scale16by8(0xffff >> i, 0xff >> j), 0xffff >> total_bitshift);
}
}
}
TEST_CASE("bit equivalence") {
// tests that 8bit and 16bit are equivalent
uint8_t r = 0xff;
uint8_t r_scale = 0xff / 2;
uint8_t brightness = 0xff / 2;
uint16_t r_scale16 = map8_to_16(r_scale);
uint16_t brightness16 = map8_to_16(brightness);
uint16_t r16 = scale16by8(scale16(r_scale16, brightness16), r);
uint8_t r8 = scale8(scale8(r_scale, brightness), r);
CHECK_EQ(r16 >> 8, r8);
}
TEST_CASE("sqrt16") {
float f = sqrt(.5) * 0xff;
uint8_t result = sqrt16(map8_to_16(0xff / 2));
CHECK_EQ(int(f), result);
CHECK_EQ(sqrt8(0xff / 2), result);
}

View file

@ -0,0 +1,46 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "fl/geometry.h"
using namespace fl;
#define REQUIRE_APPROX(a, b, tolerance) \
do { \
if (fabs((a - b)) > (tolerance)) { \
std::cerr << "REQUIRE_APPROX failed: " << #a << " = " << (a) \
<< ", " << #b << " = " << (b) \
<< ", tolerance = " << (tolerance) << std::endl; \
exit(1); \
} \
} while (0)
TEST_CASE("0 is 0 distance from diagonal line through the center") {
line_xy<float> line(-100, -100, 100, 100);
vec2<float> p(0, 0);
vec2<float> projected;
float dist = line.distance_to(p, &projected);
REQUIRE_APPROX(projected.x, 0.0f, 0.001f);
REQUIRE_APPROX(projected.y, 0.0f, 0.001f);
REQUIRE_APPROX(dist, 0.0f, 0.001f);
}
TEST_CASE("point closest to line") {
line_xy<float> line(-100, -100, 100, 100);
vec2<float> p(50, 0);
vec2<float> projected;
float dist = line.distance_to(p, &projected);
// The closest point should be (25, 25)
REQUIRE_APPROX(projected.x, 25.0f, 0.001f);
REQUIRE_APPROX(projected.y, 25.0f, 0.001f);
// Distance should be sqrt((50-25)^2 + (0-25)^2) = sqrt(1250) ≈ 35.355
REQUIRE_APPROX(dist, 35.355f, 0.001f);
}

View file

@ -0,0 +1,103 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "fl/priority_queue.h"
using namespace fl;
TEST_CASE("Simple Priority Queue") {
PriorityQueue<int> pq;
// Test empty queue
CHECK(pq.empty());
CHECK(pq.size() == 0);
// Test push and top
pq.push(5);
CHECK(!pq.empty());
CHECK(pq.size() == 1);
CHECK(pq.top() == 5);
// Test priority ordering (default is max heap)
pq.push(10);
CHECK(pq.size() == 2);
CHECK(pq.top() == 10);
pq.push(3);
CHECK(pq.size() == 3);
CHECK(pq.top() == 10);
pq.push(15);
CHECK(pq.size() == 4);
CHECK(pq.top() == 15);
// Test pop
pq.pop();
CHECK(pq.size() == 3);
CHECK(pq.top() == 10);
pq.pop();
CHECK(pq.size() == 2);
CHECK(pq.top() == 5);
pq.pop();
CHECK(pq.size() == 1);
CHECK(pq.top() == 3);
pq.pop();
CHECK(pq.empty());
CHECK(pq.size() == 0);
}
TEST_CASE("Priority Queue with Custom Type") {
struct Task {
int priority;
const char* name;
bool operator<(const Task& other) const {
return priority < other.priority;
}
};
PriorityQueue<Task> pq;
pq.push({1, "Low priority task"});
pq.push({5, "Medium priority task"});
pq.push({10, "High priority task"});
CHECK(pq.size() == 3);
CHECK(pq.top().priority == 10);
CHECK(pq.top().name == "High priority task");
pq.pop();
CHECK(pq.top().priority == 5);
CHECK(pq.top().name == "Medium priority task");
}
TEST_CASE("Priority Queue with Custom Comparator") {
struct MinHeapCompare {
bool operator()(int a, int b) const {
return a > b; // For min heap
}
};
// Create a min heap using custom heap operations
HeapVector<int> vec;
vec.push_back(5);
vec.push_back(10);
vec.push_back(3);
// Use our custom heap functions with a custom comparator
push_heap(vec.begin(), vec.end(), MinHeapCompare());
CHECK(vec[0] == 3); // Min element should be at the top
vec.push_back(1);
push_heap(vec.begin(), vec.end(), MinHeapCompare());
CHECK(vec[0] == 1); // New min element
// Test pop_heap with custom comparator
pop_heap(vec.begin(), vec.end(), MinHeapCompare());
CHECK(vec[0] == 3); // Next min element
vec.pop_back(); // Remove the element we popped to the end
}

View file

@ -0,0 +1,32 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "FastLED.h"
#include "fl/raster.h"
#include "fl/tile2x2.h"
#include "fl/xypath.h"
#include "fl/namespace.h"
using namespace fl;
TEST_CASE("XYRasterU8SparseTest should match bounds of pixels draw area") {
XYPathPtr path = XYPath::NewLinePath(-1, -1, 1, 1);
path->setDrawBounds(4,4);
Tile2x2_u8 sp0 = path->at_subpixel(0);
Tile2x2_u8 sp1 = path->at_subpixel(1);
Tile2x2_u8 subpixels[2] = {sp0, sp1};
MESSAGE("subpixels[0] = " << subpixels[0]);
MESSAGE("subpixels[1] = " << subpixels[1]);
XYRasterU8Sparse raster(4, 4);
raster.rasterize(subpixels);
auto obligatory_bounds = raster.bounds();
REQUIRE_EQ(rect<uint16_t>(0, 0, 4, 4), obligatory_bounds);
auto pixel_bounds = raster.bounds_pixels();
REQUIRE_EQ(rect<uint16_t>(0, 0, 4, 4), pixel_bounds);
}

View file

@ -0,0 +1,205 @@
// g++ --std=c++11 test.cpp
#include "fl/rectangular_draw_buffer.h"
#include "rgbw.h"
#include "test.h"
using namespace fl;
TEST_CASE("Rectangular Buffer") {
RectangularDrawBuffer buffer;
SUBCASE("Empty buffer has no LEDs") {
CHECK(buffer.getTotalBytes() == 0);
CHECK(buffer.getMaxBytesInStrip() == 0);
}
SUBCASE("Add one strip of 10 RGB LEDs") {
buffer.queue(DrawItem(1, 10, false));
CHECK(buffer.getMaxBytesInStrip() == 30);
CHECK(buffer.getTotalBytes() == 30);
}
SUBCASE("Add two strips of 10 RGB LEDs") {
buffer.queue(DrawItem(1, 10, false));
buffer.queue(DrawItem(2, 10, false));
CHECK(buffer.getMaxBytesInStrip() == 30);
CHECK(buffer.getTotalBytes() == 60);
}
SUBCASE("Add one strip of 10 RGBW LEDs") {
buffer.queue(DrawItem(1, 10, true));
uint32_t num_bytes = Rgbw::size_as_rgb(10) * 3;
CHECK(buffer.getMaxBytesInStrip() == num_bytes);
CHECK(buffer.getTotalBytes() == num_bytes);
}
SUBCASE("Add one strip of 10 RGBW LEDs and one strip of 10 RGB LEDs") {
buffer.queue(DrawItem(1, 10, true));
buffer.queue(DrawItem(2, 10, false));
uint32_t max_size_strip_bytes = Rgbw::size_as_rgb(10) * 3;
CHECK(buffer.getMaxBytesInStrip() == max_size_strip_bytes);
CHECK(buffer.getTotalBytes() == max_size_strip_bytes * 2);
}
};
TEST_CASE("Rectangular Buffer queue tests") {
RectangularDrawBuffer buffer;
SUBCASE("Queueing start and done") {
CHECK(buffer.mQueueState == RectangularDrawBuffer::IDLE);
buffer.onQueuingStart();
CHECK(buffer.mQueueState == RectangularDrawBuffer::QUEUEING);
buffer.onQueuingDone();
CHECK(buffer.mQueueState == RectangularDrawBuffer::QUEUE_DONE);
buffer.onQueuingStart();
CHECK(buffer.mQueueState == RectangularDrawBuffer::QUEUEING);
}
SUBCASE("Queue and then draw") {
buffer.onQueuingStart();
buffer.queue(DrawItem(1, 10, false));
buffer.queue(DrawItem(2, 10, false));
buffer.onQueuingDone();
CHECK(buffer.mPinToLedSegment.size() == 2);
CHECK(buffer.mAllLedsBufferUint8Size == 60);
fl::Slice<uint8_t> slice1 = buffer.getLedsBufferBytesForPin(1, true);
fl::Slice<uint8_t> slice2 = buffer.getLedsBufferBytesForPin(2, true);
// Expect that the address of slice1 happens before slice2 in memory.
CHECK(slice1.data() < slice2.data());
// Check that the size of each slice is 30 bytes.
CHECK(slice1.size() == 30);
CHECK(slice2.size() == 30);
// Check that the uint8_t buffer is zeroed out.
for (size_t i = 0; i < slice1.size(); ++i) {
REQUIRE(slice1[i] == 0);
REQUIRE(slice2[i] == 0);
}
// now fill slice1 with 0x1, slice2 with 0x2
for (size_t i = 0; i < slice1.size(); i += 3) {
slice1[i] = 0x1;
slice2[i] = 0x2;
}
// Check that the uint8_t buffer is filled with 0x1 and 0x2.
uint8_t *all_leds = buffer.mAllLedsBufferUint8.get();
uint32_t n_bytes = buffer.mAllLedsBufferUint8Size;
for (size_t i = 0; i < n_bytes; i += 3) {
if (i < slice1.size()) {
REQUIRE(all_leds[i] == 0x1);
} else {
REQUIRE(all_leds[i] == 0x2);
}
}
// bonus, test the slice::pop_front() works as expected, this time fill
// with 0x3 and 0x4
while (!slice1.empty()) {
slice1[0] = 0x3;
slice1.pop_front();
}
while (!slice2.empty()) {
slice2[0] = 0x4;
slice2.pop_front();
}
// Check that the uint8_t buffer is filled with 0x3 and 0x4.
for (size_t i = 0; i < 60; ++i) {
if (i < 30) {
REQUIRE(all_leds[i] == 0x3);
} else {
REQUIRE(all_leds[i] == 0x4);
}
}
}
SUBCASE("Test that the order that the pins are added are preserved") {
buffer.onQueuingStart();
buffer.queue(DrawItem(2, 10, false));
buffer.queue(DrawItem(1, 10, false));
buffer.queue(DrawItem(3, 10, false));
buffer.onQueuingDone();
CHECK(buffer.mPinToLedSegment.size() == 3);
CHECK(buffer.mAllLedsBufferUint8Size == 90);
fl::Slice<uint8_t> slice1 = buffer.getLedsBufferBytesForPin(2, true);
fl::Slice<uint8_t> slice2 = buffer.getLedsBufferBytesForPin(1, true);
fl::Slice<uint8_t> slice3 = buffer.getLedsBufferBytesForPin(3, true);
// Expect that the address of slice1 happens before slice2 in memory.
CHECK(slice1.data() < slice2.data());
CHECK(slice2.data() < slice3.data());
// Check that the end byte of slice1 is the first byte of slice2
CHECK(slice1.data() + slice1.size() == slice2.data());
// Check that the end byte of slice2 is the first byte of slice3
CHECK(slice2.data() + slice2.size() == slice3.data());
// Check that the ptr of the first byte of slice1 is the same as the ptr
// of the first byte of the buffer
CHECK(slice1.data() == buffer.mAllLedsBufferUint8.get());
// check that the start address is aligned to 4 bytes
CHECK((reinterpret_cast<uintptr_t>(slice1.data()) & 0x3) == 0);
}
SUBCASE("Complex test where all strip data is confirmed to be inside the "
"buffer block") {
buffer.onQueuingStart();
buffer.queue(DrawItem(1, 10, true));
buffer.queue(DrawItem(2, 11, false));
buffer.queue(DrawItem(3, 12, true));
buffer.queue(DrawItem(4, 13, false));
buffer.queue(DrawItem(5, 14, true));
buffer.queue(DrawItem(6, 15, false));
buffer.queue(DrawItem(7, 16, true));
buffer.queue(DrawItem(8, 17, false));
buffer.queue(DrawItem(9, 18, true));
buffer.onQueuingDone();
CHECK(buffer.mPinToLedSegment.size() == 9);
uint32_t expected_max_strip_bytes = Rgbw::size_as_rgb(18) * 3;
uint32_t actual_max_strip_bytes = buffer.getMaxBytesInStrip();
CHECK(actual_max_strip_bytes == expected_max_strip_bytes);
uint32_t expected_total_bytes = expected_max_strip_bytes * 9;
uint32_t actual_total_bytes = buffer.getTotalBytes();
CHECK(actual_total_bytes == expected_total_bytes);
uint8_t pins[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
for (uint8_t pin : pins) {
fl::Slice<uint8_t> slice =
buffer.getLedsBufferBytesForPin(pin, true);
CHECK(slice.size() == expected_max_strip_bytes);
const uint8_t *first_address = &slice.front();
const uint8_t *last_address = &slice.back();
// check that they are both in the buffer
CHECK(first_address >= buffer.mAllLedsBufferUint8.get());
CHECK(first_address <= buffer.mAllLedsBufferUint8.get() +
buffer.mAllLedsBufferUint8Size);
CHECK(last_address >= buffer.mAllLedsBufferUint8.get());
CHECK(last_address <= buffer.mAllLedsBufferUint8.get() +
buffer.mAllLedsBufferUint8Size);
}
}
SUBCASE("I2S test where we load 16 X 256 leds") {
buffer.onQueuingStart();
for (int i = 0; i < 16; i++) {
buffer.queue(DrawItem(i, 256, false));
}
buffer.onQueuingDone();
CHECK(buffer.mPinToLedSegment.size() == 16);
for (uint32_t i = 0; i < buffer.mAllLedsBufferUint8Size; ++i) {
buffer.mAllLedsBufferUint8[i] = i % 256;
}
}
};

View file

@ -0,0 +1,234 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/ptr.h"
#include "fl/namespace.h"
using namespace fl;
class MyClass;
typedef Ptr<MyClass> MyClassPtr;
class MyClass : public fl::Referent {
public:
MyClass() {}
~MyClass() {
destructor_signal = 0xdeadbeef;
}
virtual void ref() const override { Referent::ref(); }
virtual void unref() const override { Referent::unref(); }
virtual void destroy() const override { Referent::destroy(); }
uint32_t destructor_signal = 0;
};
TEST_CASE("Ptr basic functionality") {
Ptr<MyClass> ptr = MyClassPtr::New();
SUBCASE("Ptr is not null after construction") {
CHECK(ptr.get() != nullptr);
}
SUBCASE("Ptr increments reference count") {
CHECK(ptr->ref_count() == 1);
}
SUBCASE("Ptr can be reassigned") {
Ptr<MyClass> ptr2 = ptr;
CHECK(ptr2.get() == ptr.get());
CHECK(ptr->ref_count() == 2);
}
}
TEST_CASE("Ptr move semantics") {
SUBCASE("Move constructor works correctly") {
Ptr<MyClass> ptr1 = MyClassPtr::New();
MyClass *rawPtr = ptr1.get();
Ptr<MyClass> ptr2(std::move(ptr1));
CHECK(ptr2.get() == rawPtr);
CHECK(ptr1.get() == nullptr);
CHECK(ptr2->ref_count() == 1);
}
SUBCASE("Move assignment works correctly") {
Ptr<MyClass> ptr1 = MyClassPtr::New();
MyClass *rawPtr = ptr1.get();
Ptr<MyClass> ptr2;
ptr2 = std::move(ptr1);
CHECK(ptr2.get() == rawPtr);
CHECK(ptr1.get() == nullptr);
CHECK(ptr2->ref_count() == 1);
}
}
TEST_CASE("Ptr reference counting") {
SUBCASE("Reference count increases when copied") {
Ptr<MyClass> ptr1 = MyClassPtr::New();
Ptr<MyClass> ptr2 = ptr1;
CHECK(ptr1->ref_count() == 2);
CHECK(ptr2->ref_count() == 2);
}
SUBCASE("Reference count decreases when Ptr goes out of scope") {
Ptr<MyClass> ptr1 = MyClassPtr::New();
{
Ptr<MyClass> ptr2 = ptr1;
CHECK(ptr1->ref_count() == 2);
}
CHECK(ptr1->ref_count() == 1);
}
}
TEST_CASE("Ptr reset functionality") {
SUBCASE("Reset to nullptr") {
Ptr<MyClass> ptr = Ptr<MyClass>::New();
CHECK_EQ(1, ptr->ref_count());
ptr->ref();
CHECK_EQ(2, ptr->ref_count());
MyClass *originalPtr = ptr.get();
ptr.reset();
CHECK_EQ(1, originalPtr->ref_count());
CHECK(ptr.get() == nullptr);
CHECK(originalPtr->ref_count() == 1);
originalPtr->unref();
}
}
TEST_CASE("Ptr from static memory") {
MyClass staticObject;
{
MyClassPtr ptr = MyClassPtr::NoTracking(staticObject);
}
CHECK_EQ(staticObject.ref_count(), 0);
CHECK_NE(staticObject.destructor_signal, 0xdeadbeef);
}
TEST_CASE("WeakPtr functionality") {
WeakPtr<MyClass> weakPtr;
MyClassPtr strongPtr = MyClassPtr::New();
weakPtr = strongPtr;
REQUIRE_EQ(strongPtr->ref_count(), 1);
bool expired = weakPtr.expired();
REQUIRE_FALSE(expired);
REQUIRE(weakPtr != nullptr);
SUBCASE("WeakPtr can be locked to get a strong reference") {
MyClassPtr lockedPtr = weakPtr.lock();
CHECK(lockedPtr.get() == strongPtr.get());
CHECK_EQ(strongPtr->ref_count(), 2);
}
weakPtr.reset();
expired = weakPtr.expired();
CHECK(expired);
}
TEST_CASE("WeakPtr functionality early expiration") {
WeakPtr<MyClass> weakPtr;
MyClassPtr strongPtr = MyClassPtr::New();
weakPtr = strongPtr;
REQUIRE_EQ(strongPtr->ref_count(), 1);
bool expired = weakPtr.expired();
REQUIRE_FALSE(expired);
REQUIRE(weakPtr != nullptr);
SUBCASE("WeakPtr can be locked to get a strong reference") {
MyClassPtr lockedPtr = weakPtr.lock();
CHECK(lockedPtr.get() == strongPtr.get());
CHECK_EQ(strongPtr->ref_count(), 2);
}
strongPtr.reset();
expired = weakPtr.expired();
CHECK(expired);
}
TEST_CASE("WeakPtr additional functionality") {
SUBCASE("WeakPtr default constructor") {
WeakPtr<MyClass> weakPtr;
CHECK(weakPtr.expired());
CHECK(weakPtr.lock() == nullptr);
}
SUBCASE("WeakPtr assignment and reset") {
MyClassPtr strongRef1 = MyClassPtr::New();
MyClassPtr strongRef2 = MyClassPtr::New();
WeakPtr<MyClass> weakPtr = strongRef1;
CHECK_FALSE(weakPtr.expired());
CHECK(weakPtr.lock().get() == strongRef1.get());
weakPtr = strongRef2;
CHECK_FALSE(weakPtr.expired());
CHECK(weakPtr.lock().get() == strongRef2.get());
weakPtr.reset();
CHECK(weakPtr.expired());
CHECK(weakPtr.lock() == nullptr);
}
SUBCASE("WeakPtr multiple instances") {
MyClassPtr strongPtr = MyClassPtr::New();
WeakPtr<MyClass> weakRef1 = strongPtr;
WeakPtr<MyClass> weakRef2 = strongPtr;
CHECK_FALSE(weakRef1.expired());
CHECK_FALSE(weakRef2.expired());
CHECK(weakRef1.lock().get() == weakRef2.lock().get());
strongPtr.reset();
CHECK(weakRef1.expired());
CHECK(weakRef2.expired());
}
SUBCASE("WeakPtr with temporary strong pointer") {
WeakPtr<MyClass> weakPtr;
{
MyClassPtr tempStrongPtr = MyClassPtr::New();
weakPtr = tempStrongPtr;
CHECK_FALSE(weakPtr.expired());
}
CHECK(weakPtr.expired());
}
SUBCASE("WeakPtr lock performance") {
MyClassPtr strongPtr = MyClassPtr::New();
WeakPtr<MyClass> weakPtr = strongPtr;
for (int i = 0; i < 1000; ++i) {
MyClassPtr lockedPtr = weakPtr.lock();
CHECK(lockedPtr.get() == strongPtr.get());
}
CHECK_EQ(strongPtr->ref_count(), 1);
}
SUBCASE("WeakPtr with inheritance") {
class DerivedClass : public MyClass {};
Ptr<DerivedClass> derivedPtr = Ptr<DerivedClass>::New();
WeakPtr<MyClass> weakBasePtr = derivedPtr;
WeakPtr<DerivedClass> weakDerivedPtr = derivedPtr;
CHECK_FALSE(weakBasePtr.expired());
CHECK_FALSE(weakDerivedPtr.expired());
CHECK(weakBasePtr.lock().get() == weakDerivedPtr.lock().get());
}
}
TEST_CASE("PtrNew") {
MyClassPtr ptr = NewPtr<MyClass>(); // compile test.
CHECK(ptr.get() != nullptr);
MyClass stack_objects;
MyClassPtr stack_ptr = NewPtrNoTracking<MyClass>(stack_objects);
CHECK(stack_ptr.get() == &stack_objects);
}

View file

@ -0,0 +1,159 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/screenmap.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
using fl::Str;
TEST_CASE("ScreenMap basic functionality") {
// Create a screen map for 3 LEDs
ScreenMap map(3);
// Set some x,y coordinates
map.set(0, {1.0f, 2.0f});
map.set(1, {3.0f, 4.0f});
map.set(2, {5.0f, 6.0f});
// Test coordinate retrieval
CHECK(map[0].x == 1.0f);
CHECK(map[0].y == 2.0f);
CHECK(map[1].x == 3.0f);
CHECK(map[1].y == 4.0f);
CHECK(map[2].x == 5.0f);
CHECK(map[2].y == 6.0f);
// Test length
CHECK(map.getLength() == 3);
// Test diameter (default should be -1.0)
CHECK(map.getDiameter() == -1.0f);
// Test mapToIndex (should give same results as operator[])
auto coords = map.mapToIndex(1);
CHECK(coords.x == 3.0f);
CHECK(coords.y == 4.0f);
}
TEST_CASE("ScreenMap JSON parsing") {
const char* json = R"({
"map": {
"strip1": {
"x": [10.5, 30.5, 50.5],
"y": [20.5, 40.5, 60.5],
"diameter": 2.5
},
"strip2": {
"x": [15.0, 35.0],
"y": [25.0, 45.0],
"diameter": 1.5
}
}
})";
fl::FixedMap<Str, ScreenMap, 16> segmentMaps;
ScreenMap::ParseJson(json, &segmentMaps);
ScreenMap& strip1 = segmentMaps["strip1"];
ScreenMap& strip2 = segmentMaps["strip2"];
// Check first strip
CHECK(strip1.getLength() == 3);
CHECK(strip1.getDiameter() == 2.5f);
CHECK(strip1[0].x == 10.5f);
CHECK(strip1[0].y == 20.5f);
CHECK(strip1[1].x == 30.5f);
CHECK(strip1[1].y == 40.5f);
// Check second strip
CHECK(strip2.getLength() == 2);
CHECK(strip2.getDiameter() == 1.5f);
CHECK(strip2[0].x == 15.0f);
CHECK(strip2[0].y == 25.0f);
CHECK(strip2[1].x == 35.0f);
CHECK(strip2[1].y == 45.0f);
}
TEST_CASE("ScreenMap multiple strips JSON serialization") {
// Create a map with multiple strips
fl::FixedMap<Str, ScreenMap, 16> originalMaps;
// First strip
ScreenMap strip1(2, 2.0f);
strip1.set(0, {1.0f, 2.0f});
strip1.set(1, {3.0f, 4.0f});
originalMaps.insert("strip1", strip1);
// Second strip
ScreenMap strip2(3, 1.5f);
strip2.set(0, {10.0f, 20.0f});
strip2.set(1, {30.0f, 40.0f});
strip2.set(2, {50.0f, 60.0f});
originalMaps.insert("strip2", strip2);
// Serialize to JSON string
Str jsonStr;
ScreenMap::toJsonStr(originalMaps, &jsonStr);
// Deserialize back to a new map
fl::FixedMap<Str, ScreenMap, 16> deserializedMaps;
ScreenMap::ParseJson(jsonStr.c_str(), &deserializedMaps);
// Verify first strip
ScreenMap& deserializedStrip1 = deserializedMaps["strip1"];
CHECK(deserializedStrip1.getLength() == 2);
CHECK(deserializedStrip1.getDiameter() == 2.0f);
CHECK(deserializedStrip1[0].x == 1.0f);
CHECK(deserializedStrip1[0].y == 2.0f);
CHECK(deserializedStrip1[1].x == 3.0f);
CHECK(deserializedStrip1[1].y == 4.0f);
// Verify second strip
ScreenMap& deserializedStrip2 = deserializedMaps["strip2"];
CHECK(deserializedStrip2.getLength() == 3);
CHECK(deserializedStrip2.getDiameter() == 1.5f);
CHECK(deserializedStrip2[0].x == 10.0f);
CHECK(deserializedStrip2[0].y == 20.0f);
CHECK(deserializedStrip2[1].x == 30.0f);
CHECK(deserializedStrip2[1].y == 40.0f);
CHECK(deserializedStrip2[2].x == 50.0f);
CHECK(deserializedStrip2[2].y == 60.0f);
}
TEST_CASE("ScreenMap getBounds functionality") {
// Create a screen map with points at different coordinates
ScreenMap map(4);
map.set(0, {1.0f, 2.0f});
map.set(1, {-3.0f, 4.0f});
map.set(2, {5.0f, -6.0f});
map.set(3, {-2.0f, -1.0f});
// Get the bounds
vec2f bounds = map.getBounds();
// The bounds should be the difference between max and min values
// Max X: 5.0, Min X: -3.0 => Width = 8.0
// Max Y: 4.0, Min Y: -6.0 => Height = 10.0
CHECK(bounds.x == 8.0f);
CHECK(bounds.y == 10.0f);
// Test with a single point
ScreenMap singlePoint(1);
singlePoint.set(0, {3.5f, 4.5f});
vec2f singleBounds = singlePoint.getBounds();
CHECK(singleBounds.x == 0.0f);
CHECK(singleBounds.y == 0.0f);
// Test with an empty map
ScreenMap emptyMap(0);
vec2f emptyBounds = emptyMap.getBounds();
CHECK(emptyBounds.x == 0.0f);
CHECK(emptyBounds.y == 0.0f);
}

View file

@ -0,0 +1,33 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/sin32.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
// 16777216 is 1 cycle
const uint32_t _360 = 16777216;
const uint32_t _ONE = 2147418112;
const uint32_t _NEG_ONE = -2147418112;
TEST_CASE("compile test") {
int32_t result = sin32(0);
REQUIRE(result == 0);
result = sin32(_360);
REQUIRE(result == 0);
result = sin32(_360 / 4);
REQUIRE(result == _ONE);
result = sin32(_360 / 2);
REQUIRE(result == 0);
result = sin32(_360 / 4 * 3);
REQUIRE(result == _NEG_ONE);
}

View file

@ -0,0 +1,97 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "fl/slice.h"
#include "fl/unused.h"
#include "fl/vector.h"
#include "test.h"
using namespace fl;
TEST_CASE("vector slice") {
HeapVector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
Slice<int> slice(vec.data(), vec.size());
REQUIRE_EQ(slice.length(), 4);
REQUIRE_EQ(slice[0], 1);
REQUIRE_EQ(slice[1], 2);
REQUIRE_EQ(slice[2], 3);
REQUIRE_EQ(slice[3], 4);
Slice<int> slice2 = slice.slice(1, 3);
REQUIRE_EQ(slice2.length(), 2);
REQUIRE_EQ(slice2[0], 2);
REQUIRE_EQ(slice2[1], 3);
}
TEST_CASE("matrix compile") {
int data[2][2] = {{1, 2}, {3, 4}};
// Window from (0,0) up to (1,1)
MatrixSlice<int> slice(&data[0][0], // data pointer
2, // data width
2, // data height
0, 0, // bottom-left x,y
1, 1 // top-right x,y
);
FASTLED_UNUSED(slice);
// just a compiletime smoke test
}
TEST_CASE("matrix slice returns correct values") {
int data[2][2] = {{1, 2}, {3, 4}};
// Window from (0,0) up to (1,1)
MatrixSlice<int> slice(&data[0][0], // data pointer
2, // data width
2, // data height
0, 0, // bottom-left x,y
1, 1 // top-right x,y
);
// sanitycheck each element
REQUIRE_EQ(slice(0, 0), data[0][0]);
REQUIRE_EQ(slice(1, 0), data[0][1]);
REQUIRE_EQ(slice(0, 1), data[1][0]);
REQUIRE_EQ(slice(1, 1), data[1][1]);
// Require that the [][] operator works the same as the data
REQUIRE_EQ(slice[0][0], data[0][0]);
REQUIRE_EQ(slice[0][1], data[0][1]);
REQUIRE_EQ(slice[1][0], data[1][0]);
REQUIRE_EQ(slice[1][1], data[1][1]);
}
TEST_CASE("4x4 matrix slice returns correct values") {
int data[4][4] = {
{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 16}};
// Take a 2×2 window from (1,1) up to (2,2)
MatrixSlice<int> slice(&data[0][0], // data pointer
4, // data width
4, // data height
1, 1, // bottom-left x,y
2, 2 // top-right x,y
);
// test array access
REQUIRE_EQ(slice[0][0], data[1][1]);
REQUIRE_EQ(slice[0][1], data[1][2]);
REQUIRE_EQ(slice[1][0], data[2][1]);
REQUIRE_EQ(slice[1][1], data[2][2]);
// Remember that array access is row-major, so data[y][x] == slice(x,y)
REQUIRE_EQ(slice(0, 0), data[1][1]);
REQUIRE_EQ(slice(1, 0), data[1][2]);
REQUIRE_EQ(slice(0, 1), data[2][1]);
REQUIRE_EQ(slice(1, 1), data[2][2]);
}

View file

@ -0,0 +1,151 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/splat.h"
#include "fl/namespace.h"
using namespace fl;
TEST_CASE("splat simple test") {
// Define a simple input coordinate
vec2f input(0, 0);
// Call the splat function
Tile2x2_u8 result = splat(input);
REQUIRE(result.bounds().mMin.x == 0);
REQUIRE(result.bounds().mMin.y == 0);
REQUIRE(result.bounds().mMax.x == 2);
REQUIRE(result.bounds().mMax.y == 2);
// Verify the output
REQUIRE_EQ(result.lower_left(), 255); // Expected intensity for lower-left
REQUIRE_EQ(result.lower_right(), 0); // Expected intensity for lower-right
REQUIRE_EQ(result.upper_left(), 0); // Expected intensity for upper-left
REQUIRE_EQ(result.upper_right(), 0); // Expected intensity for upper-right
}
TEST_CASE("splat test for input (0.0, 0.5)") {
// Define the input coordinate
vec2f input(0.0f, 0.5f);
// Call the splat function
Tile2x2_u8 result = splat(input);
// Verify the bounds of the tile
REQUIRE(result.bounds().mMin.x == 0);
REQUIRE(result.bounds().mMin.y == 0);
REQUIRE(result.bounds().mMax.x == 2);
REQUIRE(result.bounds().mMax.y == 2);
// Verify the output intensities
REQUIRE_EQ(result.lower_left(), 128); // Expected intensity for lower-left
REQUIRE_EQ(result.lower_right(), 0); // Expected intensity for lower-right
REQUIRE_EQ(result.upper_left(), 128); // Expected intensity for upper-left
REQUIRE_EQ(result.upper_right(), 0); // Expected intensity for upper-right
}
TEST_CASE("splat test for input (0.0, 0.99)") {
// Define the input coordinate
vec2f input(0.0f, 0.99f);
// Call the splat function
Tile2x2_u8 result = splat(input);
// Verify the bounds of the tile
REQUIRE(result.bounds().mMin.x == 0);
REQUIRE(result.bounds().mMin.y == 0);
REQUIRE(result.bounds().mMax.x == 2);
REQUIRE(result.bounds().mMax.y == 2);
// Verify the output intensities
REQUIRE_EQ(result.lower_left(), 3); // Expected intensity for lower-left
REQUIRE_EQ(result.lower_right(), 0); // Expected intensity for lower-right
REQUIRE_EQ(result.upper_left(), 252); // Expected intensity for upper-left
REQUIRE_EQ(result.upper_right(), 0); // Expected intensity for upper-right
}
TEST_CASE("splat test for input (0.0, 1.0)") {
// Define the input coordinate
vec2f input(0.0f, 1.0f);
// Call the splat function
Tile2x2_u8 result = splat(input);
// Verify the bounds of the tile
REQUIRE(result.bounds().mMin.x == 0);
REQUIRE(result.bounds().mMin.y == 1);
REQUIRE(result.bounds().mMax.x == 2);
REQUIRE(result.bounds().mMax.y == 3);
// Verify the output intensities
REQUIRE_EQ(result.lower_left(), 255); // Expected intensity for lower-left
REQUIRE_EQ(result.lower_right(), 0); // Expected intensity for lower-right
REQUIRE_EQ(result.upper_left(), 0); // Expected intensity for upper-left
REQUIRE_EQ(result.upper_right(), 0); // Expected intensity for upper-right
}
TEST_CASE("splat test for input (0.5, 0.0)") {
// Define the input coordinate
vec2f input(0.5f, 0.0f);
// Call the splat function
Tile2x2_u8 result = splat(input);
// Verify the bounds of the tile
REQUIRE(result.bounds().mMin.x == 0);
REQUIRE(result.bounds().mMin.y == 0);
REQUIRE(result.bounds().mMax.x == 2);
REQUIRE(result.bounds().mMax.y == 2);
// Verify the output intensities
REQUIRE_EQ(result.lower_left(), 128); // Expected intensity for lower-left
REQUIRE_EQ(result.lower_right(), 128); // Expected intensity for lower-right
REQUIRE_EQ(result.upper_left(), 0); // Expected intensity for upper-left
REQUIRE_EQ(result.upper_right(), 0); // Expected intensity for upper-right
}
TEST_CASE("splat test for input (0.99, 0.0)") {
// Define the input coordinate
vec2f input(0.99f, 0.0f);
// Call the splat function
Tile2x2_u8 result = splat(input);
// Verify the bounds of the tile
REQUIRE(result.bounds().mMin.x == 0);
REQUIRE(result.bounds().mMin.y == 0);
REQUIRE(result.bounds().mMax.x == 2);
REQUIRE(result.bounds().mMax.y == 2);
// Verify the output intensities
REQUIRE_EQ(result.lower_left(), 3); // Expected intensity for lower-left
REQUIRE_EQ(result.lower_right(), 252); // Expected intensity for lower-right
REQUIRE_EQ(result.upper_left(), 0); // Expected intensity for upper-left
REQUIRE_EQ(result.upper_right(), 0); // Expected intensity for upper-right
}
TEST_CASE("splat test for input (1.0, 0.0)") {
// Define the input coordinate
vec2f input(1.0f, 0.0f);
// Call the splat function
Tile2x2_u8 result = splat(input);
// Verify the bounds of the tile
REQUIRE(result.bounds().mMin.x == 1);
REQUIRE(result.bounds().mMin.y == 0);
REQUIRE(result.bounds().mMax.x == 3);
REQUIRE(result.bounds().mMax.y == 2);
// Verify the output intensities
REQUIRE_EQ(result.lower_left(), 255); // Expected intensity for lower-left
REQUIRE_EQ(result.lower_right(), 0); // Expected intensity for lower-right
REQUIRE_EQ(result.upper_left(), 0); // Expected intensity for upper-left
REQUIRE_EQ(result.upper_right(), 0); // Expected intensity for upper-right
}

View file

@ -0,0 +1,147 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/str.h"
#include "fl/vector.h"
#include "crgb.h"
#include <sstream>
#include "fl/namespace.h"
using namespace fl;
TEST_CASE("Str basic operations") {
SUBCASE("Construction and assignment") {
Str s1;
CHECK(s1.size() == 0);
CHECK(s1.c_str()[0] == '\0');
Str s2("hello");
CHECK(s2.size() == 5);
CHECK(strcmp(s2.c_str(), "hello") == 0);
Str s3 = s2;
CHECK(s3.size() == 5);
CHECK(strcmp(s3.c_str(), "hello") == 0);
s1 = "world";
CHECK(s1.size() == 5);
CHECK(strcmp(s1.c_str(), "world") == 0);
}
SUBCASE("Comparison operators") {
Str s1("hello");
Str s2("hello");
Str s3("world");
CHECK(s1 == s2);
CHECK(s1 != s3);
}
SUBCASE("Indexing") {
Str s("hello");
CHECK(s[0] == 'h');
CHECK(s[4] == 'o');
CHECK(s[5] == '\0'); // Null terminator
}
SUBCASE("Append") {
Str s("hello");
s.append(" world");
CHECK(s.size() == 11);
CHECK(strcmp(s.c_str(), "hello world") == 0);
}
SUBCASE("CRGB to Str") {
CRGB c(255, 0, 0);
Str s = c.toString();
CHECK_EQ(s, "CRGB(255,0,0)");
}
SUBCASE("Copy-on-write behavior") {
Str s1("hello");
Str s2 = s1;
s2.append(" world");
CHECK(strcmp(s1.c_str(), "hello") == 0);
CHECK(strcmp(s2.c_str(), "hello world") == 0);
}
}
TEST_CASE("Str::reserve") {
Str s;
s.reserve(10);
CHECK(s.size() == 0);
CHECK(s.capacity() >= 10);
s.reserve(5);
CHECK(s.size() == 0);
CHECK(s.capacity() >= 10);
s.reserve(500);
CHECK(s.size() == 0);
CHECK(s.capacity() >= 500);
// s << "hello";
s.append("hello");
CHECK(s.size() == 5);
CHECK_EQ(s, "hello");
}
TEST_CASE("Str with fl::FixedVector") {
fl::FixedVector<Str, 10> vec;
vec.push_back(Str("hello"));
vec.push_back(Str("world"));
CHECK(vec.size() == 2);
CHECK(strcmp(vec[0].c_str(), "hello") == 0);
CHECK(strcmp(vec[1].c_str(), "world") == 0);
}
TEST_CASE("Str with long strings") {
const char* long_string = "This is a very long string that exceeds the inline buffer size and should be allocated on the heap";
Str s(long_string);
CHECK(s.size() == strlen(long_string));
CHECK(strcmp(s.c_str(), long_string) == 0);
Str s2 = s;
CHECK(s2.size() == strlen(long_string));
CHECK(strcmp(s2.c_str(), long_string) == 0);
s2.append(" with some additional text");
CHECK(strcmp(s.c_str(), long_string) == 0); // Original string should remain unchanged
}
TEST_CASE("Str overflowing inline data") {
SUBCASE("Construction with long string") {
std::string long_string(FASTLED_STR_INLINED_SIZE + 10, 'a'); // Create a string longer than the inline buffer
Str s(long_string.c_str());
CHECK(s.size() == long_string.length());
CHECK(strcmp(s.c_str(), long_string.c_str()) == 0);
}
SUBCASE("Appending to overflow") {
Str s("Short string");
std::string append_string(FASTLED_STR_INLINED_SIZE, 'b'); // String to append that will cause overflow
s.append(append_string.c_str());
CHECK(s.size() == strlen("Short string") + append_string.length());
CHECK(s[0] == 'S');
CHECK(s[s.size() - 1] == 'b');
}
SUBCASE("Copy on write with long string") {
std::string long_string(FASTLED_STR_INLINED_SIZE + 20, 'c');
Str s1(long_string.c_str());
Str s2 = s1;
CHECK(s1.size() == s2.size());
CHECK(strcmp(s1.c_str(), s2.c_str()) == 0);
s2.append("extra");
CHECK(s1.size() == long_string.length());
CHECK(s2.size() == long_string.length() + 5);
CHECK(strcmp(s1.c_str(), long_string.c_str()) == 0);
CHECK(s2[s2.size() - 1] == 'a');
}
}

View file

@ -0,0 +1,45 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "FastLED.h"
#include "cled_controller.h"
#include "platforms/wasm/strip_id_map.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
struct FakeSpi {
int value = 0;
};
class FakeCLedController : public CLEDController {
public:
FakeSpi fake_spi;
virtual void showColor(const CRGB &data, int nLeds,
uint8_t brightness) override {}
virtual void show(const struct CRGB *data, int nLeds,
uint8_t brightness) override {}
virtual void init() override {}
};
TEST_CASE("StripIdMap Simple Test") {
StripIdMap::test_clear();
FakeCLedController fake_controller;
int id = StripIdMap::addOrGetId(&fake_controller);
CHECK(id == 0);
CLEDController *owner = StripIdMap::getOwner(id);
CLEDController *match = &fake_controller;
printf("Owner: %p, Match: %p\n", owner, match);
CHECK_EQ(owner, match);
CHECK(StripIdMap::getId(&fake_controller) == 0);
id = StripIdMap::getOrFindByAddress(reinterpret_cast<uintptr_t>(&fake_controller));
CHECK(id == 0);
id = StripIdMap::getOrFindByAddress(reinterpret_cast<uintptr_t>(&fake_controller.fake_spi));
CHECK(id == 0);
}

View file

@ -0,0 +1,66 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/str.h"
#include "fl/strstream.h"
#include "fl/vector.h"
#include "crgb.h"
#include <sstream>
#include "fl/namespace.h"
using namespace fl;
TEST_CASE("StrStream basic operations") {
SUBCASE("Construction and assignment") {
StrStream s;
CHECK(s.str().size() == 0);
CHECK(s.str().c_str()[0] == '\0');
StrStream s2("hello");
CHECK(s2.str().size() == 5);
CHECK(strcmp(s2.str().c_str(), "hello") == 0);
StrStream s3 = s2;
CHECK(s3.str().size() == 5);
CHECK(strcmp(s3.str().c_str(), "hello") == 0);
s = "world";
CHECK(s.str().size() == 5);
CHECK(strcmp(s.str().c_str(), "world") == 0);
}
SUBCASE("Comparison operators") {
StrStream s1("hello");
StrStream s2("hello");
StrStream s3("world");
CHECK(s1.str() == s2.str());
CHECK(s1.str() != s3.str());
}
SUBCASE("Indexing") {
StrStream s("hello");
CHECK(s.str()[0] == 'h');
CHECK(s.str()[4] == 'o');
CHECK(s.str()[5] == '\0'); // Null terminator
}
SUBCASE("Append") {
StrStream s("hello");
s << " world";
CHECK(s.str().size() == 11);
CHECK(strcmp(s.str().c_str(), "hello world") == 0);
}
SUBCASE("CRGB to StrStream") {
CRGB c(255, 0, 0);
StrStream s;
s << c;
CHECK(s.str().size() == 13); // "CRGB(255,0,0)" is 13 chars
CHECK(strcmp(s.str().c_str(), "CRGB(255,0,0)") == 0);
}
}

View file

@ -0,0 +1,18 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/template_magic.h"
#include "fl/namespace.h"
#include <type_traits>
class Base {};
class Derived : public Base {};
TEST_CASE("is_base_of") {
CHECK(fl::is_base_of<Base, Derived>::value);
CHECK_FALSE(fl::is_base_of<Derived, Base>::value);
}

View file

@ -0,0 +1,74 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "lib8tion/intmap.h"
#include "fl/transform.h"
#include "fl/vector.h"
#include "fl/unused.h"
#include <string>
using namespace fl;
TEST_CASE("Transform16::ToBounds(max_value)") {
// Transform16 tx = Transform16::ToBounds(255);
SUBCASE("Check bounds at 128") {
// known bad at i == 128
Transform16 tx = Transform16::ToBounds(255);
uint16_t i16 = map8_to_16(128);
vec2<uint16_t> xy_input = vec2<uint16_t>(i16, i16);
vec2<uint16_t> xy = tx.transform(xy_input);
INFO("i = " << 128);
REQUIRE_EQ(128, xy.x);
REQUIRE_EQ(128, xy.y);
}
SUBCASE("Check identity from 8 -> 16") {
Transform16 tx = Transform16::ToBounds(255);
for (uint16_t i = 0; i < 256; i++) {
uint16_t i16 = map8_to_16(i);
vec2<uint16_t> xy_input = vec2<uint16_t>(i16, i16);
vec2<uint16_t> xy = tx.transform(xy_input);
INFO("i = " << i);
REQUIRE_EQ(i, xy.x);
REQUIRE_EQ(i, xy.y);
}
}
SUBCASE("Check all bounds are in 255") {
Transform16 tx = Transform16::ToBounds(255);
uint32_t smallest = ~0;
uint32_t largest = 0;
for (uint16_t i = 0; i < 256; i++) {
uint16_t i16 = map8_to_16(i);
vec2<uint16_t> xy_input = vec2<uint16_t>(i16, i16);
vec2<uint16_t> xy = tx.transform(xy_input);
INFO("i = " << i);
REQUIRE_LE(xy.x, 255);
REQUIRE_LE(xy.y, 255);
smallest = MIN(smallest, xy.x);
largest = MAX(largest, xy.x);
}
REQUIRE_EQ(0, smallest);
REQUIRE_EQ(255, largest);
}
}
TEST_CASE("Transform16::ToBounds(min, max)") {
SUBCASE("Check bounds at 128") {
uint16_t low = 127;
uint16_t high = 255 + 127;
vec2<uint16_t> min = vec2<uint16_t>(low, low);
vec2<uint16_t> max = vec2<uint16_t>(high, high);
Transform16 tx = Transform16::ToBounds(min, max);
auto t1 = tx.transform(vec2<uint16_t>(0, 0));
auto t2 = tx.transform(vec2<uint16_t>(0xffff, 0xffff));
REQUIRE_EQ(vec2<uint16_t>(low, low), t1);
REQUIRE_EQ(vec2<uint16_t>(high, high), t2);
}
}

View file

@ -0,0 +1,72 @@
// test_transition_ramp.cpp
// g++ --std=c++11 test_transition_ramp.cpp
#include "fl/namespace.h"
#include "fl/time_alpha.h"
#include "test.h"
#include <cstdint>
using namespace fl;
TEST_CASE("Test transition ramp") {
// total latch = 100 ms, rampup = 10 ms, rampdown = 10 ms
TimeRamp ramp(10, 100, 10);
uint32_t t0 = 0;
ramp.trigger(t0);
// at start: still at zero
REQUIRE(ramp.update8(t0) == 0);
// midrise: 5 ms → (5*255/10) ≈ 127
REQUIRE(ramp.update8(t0 + 5) == static_cast<uint8_t>((5 * 255) / 10));
// end of rise: 10 ms → full on
REQUIRE(ramp.update8(t0 + 10) == 255);
// plateau: well within [10, 90) ms
REQUIRE(ramp.update8(t0 + 50) == 255);
// midfall: elapsed=95 ms → fallingElapsed=5 ms → 255 - (5*255/10) ≈ 128
uint8_t value = ramp.update8(t0 + 115);
uint8_t expected = static_cast<uint8_t>(255 - (5 * 255) / 10);
REQUIRE_EQ(expected, value);
// after latch: 110 ms → off
// REQUIRE(ramp.update8(t0 + 110) == 0);
// now do it again
ramp.trigger(200);
// at start: still at zero
REQUIRE(ramp.update8(200) == 0);
// midrise: 205 ms → (5*255/10) ≈ 127
REQUIRE(ramp.update8(205) == static_cast<uint8_t>((5 * 255) / 10));
// end of rise: 210 ms → full on
REQUIRE(ramp.update8(210) == 255);
// plateau: well within [210, 290) ms
REQUIRE(ramp.update8(250) == 255);
// midfall: elapsed=295 ms → fallingElapsed=5 ms → 255 - (5*255/10) ≈ 128
REQUIRE(ramp.update8(315) == static_cast<uint8_t>(255 - (5 * 255) / 10));
// after latch: 310 ms → off
REQUIRE(ramp.update8(320) == 0);
// after latch: 410 ms → off
REQUIRE(ramp.update8(410) == 0);
}
TEST_CASE("Real world Bug") {
TimeRamp transition = TimeRamp(500, 0, 500);
uint8_t value = transition.update8(0);
CHECK(value == 0);
value = transition.update8(1);
CHECK(value == 0);
transition.trigger(6900);
value = transition.update8(6900);
REQUIRE_EQ(value, 0);
value = transition.update8(6900 + 500);
REQUIRE_EQ(value, 255);
value = transition.update8(6900 + 250);
REQUIRE_EQ(value, 127);
}

View file

@ -0,0 +1,92 @@
#include "fl/namespace.h"
#include "fl/time_alpha.h"
#include "fl/traverse_grid.h"
#include "test.h"
#include <set>
#include <utility>
#include <iostream>
using namespace fl;
struct CollectingVisitor {
std::set<std::pair<int, int>> visited;
void visit(int x, int y) {
visited.insert({x, y});
}
};
TEST_CASE("Traverse grid") {
SUBCASE("Horizontal line") {
vec2f start{1.2f, 2.5f};
vec2f end{5.7f, 2.5f};
CollectingVisitor visitor;
traverseGridSegment(start, end, visitor);
CHECK(visitor.visited.size() == 5);
for (int x = 1; x <= 5; ++x) {
CHECK(visitor.visited.count({x, 2}) == 1);
}
}
SUBCASE("Vertical line") {
vec2f start{3.4f, 1.1f};
vec2f end{3.4f, 4.9f};
CollectingVisitor visitor;
traverseGridSegment(start, end, visitor);
CHECK(visitor.visited.size() == 4);
for (int y = 1; y <= 4; ++y) {
CHECK(visitor.visited.count({3, y}) == 1);
}
}
SUBCASE("Forward diagonal") {
vec2f start{1.1f, 1.1f};
vec2f end{4.9f, 4.9f};
CollectingVisitor visitor;
traverseGridSegment(start, end, visitor);
std::set<std::pair<int, int>> expected = {
{1,1}, {1,2}, {2,2}, {2,3}, {3,3}, {3,4}, {4,4}
};
CHECK(visitor.visited.size() == expected.size());
for (const auto& cell : expected) {
CHECK(visitor.visited.count(cell) == 1);
}
}
SUBCASE("Backward diagonal") {
vec2f start{4.9f, 1.1f};
vec2f end{1.1f, 4.9f};
CollectingVisitor visitor;
traverseGridSegment(start, end, visitor);
std::set<std::pair<int, int>> expected = {
{4,1}, {4,2}, {3,2}, {3,3}, {2,3}, {2,4}, {1,4}
};
CHECK(visitor.visited.size() == expected.size());
for (const auto& cell : expected) {
CHECK(visitor.visited.count(cell) == 1);
}
}
SUBCASE("Single cell") {
vec2f start{2.2f, 3.3f};
vec2f end{2.2f, 3.3f};
CollectingVisitor visitor;
traverseGridSegment(start, end, visitor);
CHECK(visitor.visited.size() == 1);
CHECK(visitor.visited.count({2, 3}) == 1);
}
}

View file

@ -0,0 +1,150 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "fl/type_traits.h"
using namespace fl;
// Forward declarations
class TestClass;
void takeLValue(TestClass & obj);
void takeRValue(TestClass && obj);
template <typename T> void forwardValue(T && obj);
TEST_CASE("is_integral<T> value") {
// Test with integral types
REQUIRE(is_integral<bool>::value);
REQUIRE(is_integral<char>::value);
REQUIRE(is_integral<signed char>::value);
REQUIRE(is_integral<unsigned char>::value);
REQUIRE(is_integral<int>::value);
REQUIRE(is_integral<unsigned int>::value);
REQUIRE(is_integral<short>::value);
REQUIRE(is_integral<long>::value);
REQUIRE(is_integral<long long>::value);
// Test with sized types
REQUIRE(is_integral<int8_t>::value);
REQUIRE(is_integral<uint8_t>::value);
REQUIRE(is_integral<int16_t>::value);
REQUIRE(is_integral<uint16_t>::value);
REQUIRE(is_integral<int32_t>::value);
REQUIRE(is_integral<uint32_t>::value);
REQUIRE(is_integral<int64_t>::value);
REQUIRE(is_integral<uint64_t>::value);
// Test with non-integral types
REQUIRE_FALSE(is_integral<float>::value);
REQUIRE_FALSE(is_integral<double>::value);
REQUIRE_FALSE(is_integral<char *>::value);
}
TEST_CASE("Test fl::move") {
// Test with a simple class that tracks move operations
class MoveTracker {
public:
MoveTracker() : moved_from(false) {}
// Copy constructor
MoveTracker(const MoveTracker &other) : moved_from(false) {
// Regular copy
}
// Move constructor
MoveTracker(MoveTracker &&other) : moved_from(false) {
other.moved_from = true;
}
bool was_moved_from() const { return moved_from; }
private:
bool moved_from;
};
// Test 1: Basic move operation
{
MoveTracker original;
REQUIRE_FALSE(original.was_moved_from());
// Use fl::move to trigger move constructor
MoveTracker moved(fl::move(original));
// Original should be marked as moved from
REQUIRE(original.was_moved_from());
REQUIRE_FALSE(moved.was_moved_from());
}
// Test 2: Move vs copy behavior
{
MoveTracker original;
// Regular copy - shouldn't mark original as moved
MoveTracker copied(original);
REQUIRE_FALSE(original.was_moved_from());
// Move should mark as moved
MoveTracker moved(fl::move(original));
REQUIRE(original.was_moved_from());
}
}
// A simple class to test with
class TestClass {
public:
int value;
TestClass() : value(0) {}
TestClass(int v) : value(v) {}
};
// Function that takes an lvalue reference
void takeLValue(TestClass & obj) { obj.value = 42; }
// Function that takes an rvalue reference
void takeRValue(TestClass && obj) { obj.value = 100; }
// Function template that forwards its argument to an lvalue reference function
template <typename T> void forwardToLValue(T &&obj) {
takeLValue(fl::forward<T>(obj));
}
// Function template that forwards its argument to an rvalue reference function
template <typename T> void forwardValue(T && obj) {
takeRValue(fl::forward<T>(obj));
}
TEST_CASE("fl::forward preserves value categories") {
SUBCASE("Forwarding lvalues") {
TestClass obj(10);
// Should call takeLValue
forwardToLValue(obj);
REQUIRE(obj.value == 42);
// This would fail to compile if we tried to forward an lvalue to an
// rvalue reference Uncomment to see the error: forwardValue(obj); //
// This should fail at compile time
}
SUBCASE("Forwarding rvalues") {
// Should call takeRValue
forwardValue(TestClass(20));
// We can also test with a temporary
TestClass temp(30);
forwardValue(fl::move(temp));
// temp.value should now be 100, but we can't check it here since it was
// moved
}
SUBCASE("Move and forward") {
TestClass obj(50);
// Move creates an rvalue, forward preserves that
forwardValue(fl::move(obj));
// obj was moved from, so we don't make assertions about its state
}
}

View file

@ -0,0 +1,15 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/ui.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
TEST_CASE("compile ui test") {
}

View file

@ -0,0 +1,152 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "fl/ui.h"
#include "fl/variant.h"
#include "fl/optional.h"
#include "fl/str.h"
using namespace fl;
TEST_CASE("Variant tests") {
// 1) Default is empty
Variant<int, fl::Str> v;
REQUIRE(v.empty());
REQUIRE(!v.is<int>());
REQUIRE(!v.is<fl::Str>());
// 2) Emplace a T
v = 123;
REQUIRE(v.is<int>());
REQUIRE(!v.is<fl::Str>());
REQUIRE_EQ(*v.ptr<int>(), 123);
// 3) Reset back to empty
v.reset();
REQUIRE(v.empty());
// 4) Emplace a U
v = fl::Str("hello");
REQUIRE(v.is<fl::Str>());
REQUIRE(!v.is<int>());
REQUIRE(v.equals(fl::Str("hello")));
// 5) Copyconstruct preserves the U
Variant<int, fl::Str> v2(v);
REQUIRE(v2.is<fl::Str>());
fl::Str* str_ptr = v2.ptr<fl::Str>();
REQUIRE_NE(str_ptr, nullptr);
REQUIRE_EQ(*str_ptr, fl::Str("hello"));
const bool is_str = v2.is<fl::Str>();
const bool is_int = v2.is<int>();
CHECK(is_str);
CHECK(!is_int);
#if 0
// 6) Moveconstruct leaves source empty
Variant<int, fl::Str> v3(std::move(v2));
REQUIRE(v3.is<fl::Str>());
REQUIRE_EQ(v3.getU(), fl::Str("hello"));
REQUIRE(v2.isEmpty());
// 7) Copyassign
Variant<int, fl::Str> v4;
v4 = v3;
REQUIRE(v4.is<fl::Str>());
REQUIRE_EQ(v4.getU(), fl::Str("hello"));
// 8) Swap two variants
v4.emplaceT(7);
v3.swap(v4);
REQUIRE(v3.is<int>());
REQUIRE_EQ(v3.getT(), 7);
REQUIRE(v4.is<fl::Str>());
REQUIRE_EQ(v4.getU(), fl::Str("hello"));
#endif
}
TEST_CASE("Variant") {
// 1) Default is empty
Variant<int, fl::Str, double> v;
REQUIRE(v.empty());
REQUIRE(!v.is<int>());
REQUIRE(!v.is<fl::Str>());
REQUIRE(!v.is<double>());
// 2) Construct with a value
Variant<int, fl::Str, double> v1(123);
REQUIRE(v1.is<int>());
REQUIRE(!v1.is<fl::Str>());
REQUIRE(!v1.is<double>());
REQUIRE_EQ(*v1.ptr<int>(), 123);
// 3) Construct with a different type
Variant<int, fl::Str, double> v2(fl::Str("hello"));
REQUIRE(!v2.is<int>());
REQUIRE(v2.is<fl::Str>());
REQUIRE(!v2.is<double>());
REQUIRE_EQ(*v2.ptr<fl::Str>(), fl::Str("hello"));
// 4) Copy construction
Variant<int, fl::Str, double> v3(v2);
REQUIRE(v3.is<fl::Str>());
REQUIRE(v3.equals(fl::Str("hello")));
// 5) Assignment
v = v1;
REQUIRE(v.is<int>());
REQUIRE_EQ(*v.ptr<int>(), 123);
// 6) Reset
v.reset();
REQUIRE(v.empty());
// 7) Assignment of a value
v = 3.14;
REQUIRE(v.is<double>());
// REQUIRE_EQ(v.get<double>(), 3.14);
REQUIRE_EQ(*v.ptr<double>(), 3.14);
// 8) Visitor pattern
struct TestVisitor {
int result = 0;
void accept(int value) { result = value; }
void accept(const fl::Str& value) { result = value.length(); }
void accept(double value) { result = static_cast<int>(value); }
};
TestVisitor visitor;
v.visit(visitor);
REQUIRE_EQ(visitor.result, 3); // 3.14 truncated to 3
v = fl::Str("hello world");
v.visit(visitor);
REQUIRE_EQ(visitor.result, 11); // length of "hello world"
v = 42;
v.visit(visitor);
REQUIRE_EQ(visitor.result, 42);
}
// TEST_CASE("Optional") {
// Optional<int> opt;
// REQUIRE(opt.empty());
// opt = 42;
// REQUIRE(!opt.empty());
// REQUIRE_EQ(*opt.ptr(), 42);
// Optional<int> opt2 = opt;
// REQUIRE(!opt2.empty());
// REQUIRE_EQ(*opt2.ptr(), 42);
// opt2 = 100;
// REQUIRE_EQ(*opt2.ptr(), 100);
// }

View file

@ -0,0 +1,424 @@
// g++ --std=c++11 test.cpp
#include <random>
#include "test.h"
#include "fl/vector.h"
#include "fl/namespace.h"
using namespace fl;
TEST_CASE("Fixed vector simple") {
fl::FixedVector<int, 5> vec;
SUBCASE("Initial state") {
CHECK(vec.size() == 0);
CHECK(vec.capacity() == 5);
CHECK(vec.empty());
}
SUBCASE("Push back and access") {
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
CHECK(vec.size() == 3);
CHECK_FALSE(vec.empty());
CHECK(vec[0] == 10);
CHECK(vec[1] == 20);
CHECK(vec[2] == 30);
}
SUBCASE("Push back beyond capacity") {
for (int i = 0; i < 7; ++i) {
vec.push_back(i * 10);
}
CHECK(vec.size() == 5);
CHECK(vec.capacity() == 5);
CHECK(vec[4] == 40);
}
SUBCASE("Clear") {
vec.push_back(10);
vec.push_back(20);
vec.clear();
CHECK(vec.size() == 0);
CHECK(vec.empty());
}
}
TEST_CASE("Fixed vector insert") {
FASTLED_USING_NAMESPACE;
fl::FixedVector<int, 5> vec;
SUBCASE("Insert at beginning") {
vec.push_back(20);
vec.push_back(30);
bool inserted = vec.insert(vec.begin(), 10);
CHECK(inserted);
CHECK(vec.size() == 3);
CHECK(vec[0] == 10);
CHECK(vec[1] == 20);
CHECK(vec[2] == 30);
}
SUBCASE("Insert in middle") {
vec.push_back(10);
vec.push_back(30);
bool inserted = vec.insert(vec.begin() + 1, 20);
CHECK(inserted);
CHECK(vec.size() == 3);
CHECK(vec[0] == 10);
CHECK(vec[1] == 20);
CHECK(vec[2] == 30);
}
SUBCASE("Insert at end") {
vec.push_back(10);
vec.push_back(20);
bool inserted = vec.insert(vec.end(), 30);
CHECK(inserted);
CHECK(vec.size() == 3);
CHECK(vec[0] == 10);
CHECK(vec[1] == 20);
CHECK(vec[2] == 30);
}
SUBCASE("Insert when full") {
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
vec.push_back(40);
vec.push_back(50);
bool inserted = vec.insert(vec.begin() + 2, 25);
CHECK_FALSE(inserted);
CHECK(vec.size() == 5);
CHECK(vec[0] == 10);
CHECK(vec[1] == 20);
CHECK(vec[2] == 30);
CHECK(vec[3] == 40);
CHECK(vec[4] == 50);
}
}
TEST_CASE("Fixed vector find_if with predicate") {
FASTLED_USING_NAMESPACE;
fl::FixedVector<int, 5> vec;
SUBCASE("Find even number") {
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);
auto it = vec.find_if([](int n) { return n % 2 == 0; });
CHECK(it != vec.end());
CHECK(*it == 2);
}
SUBCASE("Find number greater than 3") {
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);
auto it = vec.find_if([](int n) { return n > 3; });
CHECK(it != vec.end());
CHECK(*it == 4);
}
SUBCASE("Find non-existent condition") {
vec.push_back(1);
vec.push_back(3);
vec.push_back(5);
auto it = vec.find_if([](int n) { return n % 2 == 0; });
CHECK(it == vec.end());
}
SUBCASE("Find in empty vector") {
auto it = vec.find_if([](int n) { return true; });
CHECK(it == vec.end());
}
}
TEST_CASE("fl::FixedVector construction and destruction") {
FASTLED_USING_NAMESPACE;
static int live_object_count = 0;
struct TestObject {
int value;
TestObject(int v = 0) : value(v) { ++live_object_count; }
~TestObject() { --live_object_count; }
TestObject(const TestObject& other) : value(other.value) { ++live_object_count; }
TestObject& operator=(const TestObject& other) {
value = other.value;
return *this;
}
};
SUBCASE("Construction and destruction") {
REQUIRE_EQ(0, live_object_count);
live_object_count = 0;
{
fl::FixedVector<TestObject, 3> vec;
CHECK(live_object_count == 0);
vec.push_back(TestObject(1));
vec.push_back(TestObject(2));
vec.push_back(TestObject(3));
CHECK(live_object_count == 3); // 3 objects in the vector
vec.pop_back();
CHECK(live_object_count == 2); // 2 objects left in the vector
}
// vec goes out of scope here
REQUIRE_EQ(live_object_count, 0);
}
SUBCASE("Clear") {
live_object_count = 0;
{
fl::FixedVector<TestObject, 3> vec;
vec.push_back(TestObject(1));
vec.push_back(TestObject(2));
CHECK(live_object_count == 2);
vec.clear();
CHECK(live_object_count == 0); // All objects should be destroyed after clear
}
CHECK(live_object_count == 0);
}
SUBCASE("Stress test clear, insert and remove") {
fl::vector_inlined<TestObject, 20> vec;
size_t checked_size = 0;
for (int i = 0; i < 1000; ++i) {
int random_value = rand() % 4;
switch (random_value) {
case 0:
if (!vec.full()) {
vec.push_back(TestObject(i));
++checked_size;
} else {
REQUIRE_EQ(20, vec.size());
}
break;
case 1:
if (!vec.empty()) {
vec.pop_back();
--checked_size;
} else {
REQUIRE_EQ(0, checked_size);
}
break;
case 2:
vec.clear();
checked_size = 0;
REQUIRE_EQ(0, vec.size());
break;
}
}
}
}
TEST_CASE("Fixed vector advanced") {
FASTLED_USING_NAMESPACE;
fl::FixedVector<int, 5> vec;
SUBCASE("Pop back") {
vec.push_back(10);
vec.push_back(20);
vec.pop_back();
CHECK(vec.size() == 1);
CHECK(vec[0] == 10);
}
SUBCASE("Front and back") {
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
CHECK(vec.front() == 10);
CHECK(vec.back() == 30);
}
SUBCASE("Iterator") {
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
int sum = 0;
for (auto it = vec.begin(); it != vec.end(); ++it) {
sum += *it;
}
CHECK(sum == 60);
}
SUBCASE("Erase") {
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
vec.erase(vec.begin() + 1);
CHECK(vec.size() == 2);
CHECK(vec[0] == 10);
CHECK(vec[1] == 30);
}
SUBCASE("Find and has") {
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
CHECK(vec.has(20));
CHECK_FALSE(vec.has(40));
auto it = vec.find(20);
CHECK(it != vec.end());
CHECK(*it == 20);
it = vec.find(40);
CHECK(it == vec.end());
}
}
TEST_CASE("Fixed vector with custom type") {
FASTLED_USING_NAMESPACE;
struct Point {
int x, y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
bool operator==(const Point& other) const { return x == other.x && y == other.y; }
};
fl::FixedVector<Point, 3> vec;
SUBCASE("Push and access custom type") {
vec.push_back(Point(1, 2));
vec.push_back(Point(3, 4));
CHECK(vec.size() == 2);
CHECK(vec[0].x == 1);
CHECK(vec[0].y == 2);
CHECK(vec[1].x == 3);
CHECK(vec[1].y == 4);
}
SUBCASE("Find custom type") {
vec.push_back(Point(1, 2));
vec.push_back(Point(3, 4));
auto it = vec.find(Point(3, 4));
CHECK(it != vec.end());
CHECK(it->x == 3);
CHECK(it->y == 4);
}
}
TEST_CASE("SortedVector") {
struct Less {
bool operator()(int a, int b) const { return a < b; }
};
SUBCASE("Insert maintains order") {
SortedHeapVector<int, Less> vec;
vec.insert(3);
vec.insert(1);
vec.insert(4);
vec.insert(2);
CHECK(vec.size() == 4);
CHECK(vec[0] == 1);
CHECK(vec[1] == 2);
CHECK(vec[2] == 3);
CHECK(vec[3] == 4);
}
SUBCASE("Erase removes element") {
SortedHeapVector<int, Less> vec;
vec.insert(3);
vec.insert(1);
vec.insert(4);
vec.insert(2);
vec.erase(3); // Remove the value 3
CHECK(vec.size() == 3);
CHECK_FALSE(vec.has(3)); // Verify 3 is no longer present
// Verify remaining elements are still in order
CHECK(vec[0] == 1);
CHECK(vec[1] == 2);
CHECK(vec[2] == 4);
}
SUBCASE("Insert when full") {
SortedHeapVector<int, Less> vec;
vec.setMaxSize(5);
// Fill the vector to capacity
vec.insert(1);
vec.insert(2);
vec.insert(3);
vec.insert(4);
vec.insert(5); // Max size is 5
InsertResult result;
vec.insert(6, &result); // Try to insert into full vector
CHECK_EQ(InsertResult::kMaxSize, result); // Should return false
CHECK(vec.size() == 5); // Size shouldn't change
CHECK(vec[4] == 5); // Last element should still be 5
}
SUBCASE("Erase from empty") {
SortedHeapVector<int, Less> vec;
bool ok = vec.erase(1); // Try to erase from empty vector
CHECK(!ok); // Should return false
CHECK(vec.size() == 0); // Should still be empty
CHECK(vec.empty());
ok = vec.erase(vec.end());
CHECK(!ok); // Should return false
CHECK(vec.size() == 0); // Should still be empty
CHECK(vec.empty());
ok = vec.erase(vec.begin());
CHECK(!ok); // Should return false
CHECK(vec.size() == 0); // Should still be empty
CHECK(vec.empty());
}
}
TEST_CASE("HeapVector") {
SUBCASE("resize") {
HeapVector<int> vec;
vec.resize(5);
CHECK(vec.size() == 5);
CHECK(vec.capacity() >= 5);
for (int i = 0; i < 5; ++i) {
CHECK_EQ(0, vec[i]);
}
}
}

View file

@ -0,0 +1,232 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include <vector>
#include "crgb.h"
#include "fl/bytestreammemory.h"
#include "fl/ptr.h"
#include "fx/video.h"
#include "fx/video/pixel_stream.h"
#include "lib8tion/intmap.h"
#include "test.h"
#include "fl/namespace.h"
#define FPS 30
#define FRAME_TIME 1000 / FPS
#define VIDEO_WIDTH 10
#define VIDEO_HEIGHT 10
#define LEDS_PER_FRAME VIDEO_WIDTH *VIDEO_HEIGHT
FASTLED_SMART_PTR(FakeFileHandle);
using namespace fl;
class FakeFileHandle : public FileHandle {
public:
virtual ~FakeFileHandle() {}
bool available() const override { return mPos < data.size(); }
size_t bytesLeft() const override { return data.size() - mPos; }
size_t size() const override { return data.size(); }
bool valid() const override { return true; }
size_t write(const uint8_t *src, size_t len) {
data.insert(data.end(), src, src + len);
return len;
}
size_t writeCRGB(const CRGB *src, size_t len) {
size_t bytes_written = write((const uint8_t *)src, len * 3);
return bytes_written / 3;
}
size_t read(uint8_t *dst, size_t bytesToRead) override {
size_t bytesRead = 0;
while (bytesRead < bytesToRead && mPos < data.size()) {
dst[bytesRead] = data[mPos];
bytesRead++;
mPos++;
}
return bytesRead;
}
size_t pos() const override { return mPos; }
const char *path() const override { return "fake"; }
bool seek(size_t pos) override {
this->mPos = pos;
return true;
}
void close() override {}
std::vector<uint8_t> data;
size_t mPos = 0;
};
TEST_CASE("video with memory stream") {
// Video video(LEDS_PER_FRAME, FPS);
Video video(LEDS_PER_FRAME, FPS, 1);
video.setFade(0, 0);
ByteStreamMemoryPtr memoryStream =
ByteStreamMemoryPtr::New(LEDS_PER_FRAME * 3);
CRGB testData[LEDS_PER_FRAME] = {};
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
testData[i] = i % 2 == 0 ? CRGB::Red : CRGB::Black;
}
size_t pixels_written = memoryStream->writeCRGB(testData, LEDS_PER_FRAME);
REQUIRE_EQ(pixels_written, LEDS_PER_FRAME);
video.beginStream(memoryStream);
CRGB leds[LEDS_PER_FRAME];
bool ok = video.draw(FRAME_TIME + 1, leds);
REQUIRE(ok);
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
CHECK_EQ(leds[i], testData[i]);
}
ok = video.draw(2 * FRAME_TIME + 1, leds);
REQUIRE(ok);
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
// CHECK_EQ(leds[i], testData[i]);
REQUIRE_EQ(leds[i].r, testData[i].r);
REQUIRE_EQ(leds[i].g, testData[i].g);
REQUIRE_EQ(leds[i].b, testData[i].b);
}
}
TEST_CASE("video with memory stream, interpolated") {
// Video video(LEDS_PER_FRAME, FPS);
Video video(LEDS_PER_FRAME, 1);
video.setFade(0, 0);
ByteStreamMemoryPtr memoryStream =
ByteStreamMemoryPtr::New(LEDS_PER_FRAME * sizeof(CRGB) * 2);
CRGB testData[LEDS_PER_FRAME] = {};
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
testData[i] = CRGB::Red;
}
size_t pixels_written = memoryStream->writeCRGB(testData, LEDS_PER_FRAME);
CHECK_EQ(pixels_written, LEDS_PER_FRAME);
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
testData[i] = CRGB::Black;
}
pixels_written = memoryStream->writeCRGB(testData, LEDS_PER_FRAME);
CHECK_EQ(pixels_written, LEDS_PER_FRAME);
video.beginStream(memoryStream); // One frame per second.
CRGB leds[LEDS_PER_FRAME];
bool ok = video.draw(0, leds); // First frame starts time 0.
ok = video.draw(500, leds); // Half a frame.
CHECK(ok);
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
int r = leds[i].r;
int g = leds[i].g;
int b = leds[i].b;
REQUIRE_EQ(128, r); // We expect the color to be interpolated to 128.
REQUIRE_EQ(0, g);
REQUIRE_EQ(0, b);
}
}
TEST_CASE("video with file handle") {
// Video video(LEDS_PER_FRAME, FPS);
Video video(LEDS_PER_FRAME, FPS);
video.setFade(0, 0);
FakeFileHandlePtr fileHandle = FakeFileHandlePtr::New();
CRGB led_frame[LEDS_PER_FRAME];
// alternate between red and black
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
led_frame[i] = i % 2 == 0 ? CRGB::Red : CRGB::Black;
}
// now write the data
size_t leds_written = fileHandle->writeCRGB(led_frame, LEDS_PER_FRAME);
CHECK_EQ(leds_written, LEDS_PER_FRAME);
video.begin(fileHandle);
CRGB leds[LEDS_PER_FRAME];
bool ok = video.draw(FRAME_TIME + 1, leds);
REQUIRE(ok);
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
CHECK_EQ(leds[i], led_frame[i]);
}
ok = video.draw(2 * FRAME_TIME + 1, leds);
CHECK(ok);
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
CHECK_EQ(leds[i], led_frame[i]);
}
}
TEST_CASE("Video duration") {
Video video(LEDS_PER_FRAME, FPS);
FakeFileHandlePtr fileHandle = FakeFileHandlePtr::New();
CRGB led_frame[LEDS_PER_FRAME];
// just set all the leds to white
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
led_frame[i] = CRGB::White;
}
// fill frames for all of one second
for (uint32_t i = 0; i < FPS; i++) {
size_t leds_written = fileHandle->writeCRGB(led_frame, LEDS_PER_FRAME);
CHECK_EQ(leds_written, LEDS_PER_FRAME);
}
video.begin(fileHandle);
int32_t duration = video.durationMicros();
float duration_f = duration / 1000.0;
CHECK_EQ(1000, uint32_t(duration_f + 0.5));
}
TEST_CASE("video with end frame fadeout") {
Video video(LEDS_PER_FRAME, FPS);
video.setFade(0, 1000);
FakeFileHandlePtr fileHandle = FakeFileHandlePtr::New();
CRGB led_frame[LEDS_PER_FRAME];
// just set all the leds to white
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
led_frame[i] = CRGB::White;
}
// fill frames for all of one second
for (uint32_t i = 0; i < FPS; i++) {
size_t leds_written = fileHandle->writeCRGB(led_frame, LEDS_PER_FRAME);
CHECK_EQ(leds_written, LEDS_PER_FRAME);
}
video.begin(fileHandle);
CRGB leds[LEDS_PER_FRAME];
bool ok = video.draw(0, leds);
REQUIRE(ok);
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
CHECK_EQ(leds[i], led_frame[i]);
}
ok = video.draw(500, leds);
// test that the leds are about half as bright
REQUIRE(ok);
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
// This is what the values should be but we don't do inter-frame
// interpolation yet. CHECK_EQ(leds[i].r, 127); CHECK_EQ(leds[i].g,
// 127); CHECK_EQ(leds[i].b, 127);
CHECK_EQ(leds[i].r, 110);
CHECK_EQ(leds[i].g, 110);
CHECK_EQ(leds[i].b, 110);
}
ok = video.draw(900, leds); // close to last frame
REQUIRE(ok);
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
CHECK_EQ(leds[i].r, 8);
CHECK_EQ(leds[i].g, 8);
CHECK_EQ(leds[i].b, 8);
}
ok = video.draw(965, leds); // Last frame
REQUIRE(ok);
// test that the leds are almost black
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
REQUIRE_EQ(leds[i], CRGB(0, 0, 0));
}
#if 0 // Bug - we do not handle wrapping around
ok = video.draw(1000, leds); // Bug - we have to let the buffer drain with one frame.
ok = video.draw(1000, leds); // After last frame we flip around
for (uint32_t i = 0; i < LEDS_PER_FRAME; i++) {
REQUIRE_EQ(leds[i], CRGB(4, 4, 4));
}
#endif //
}

View file

@ -0,0 +1,53 @@
#include "test.h"
#include "fx/fx.h"
#include "fx/fx2d.h"
#include "fx/video.h"
#include "fl/vector.h"
#include "FastLED.h"
FASTLED_SMART_PTR(Fake2d);
// Simple Fx2d object which writes a single red pixel to the first LED
// with the red component being the intensity of the frame counter.
class Fake2d : public Fx2d {
public:
Fake2d() : Fx2d(XYMap::constructRectangularGrid(1,1)) {}
void draw(DrawContext context) override {
CRGB c = mColors[mFrameCounter % mColors.size()];
context.leds[0] = c;
mFrameCounter++;
}
bool hasFixedFrameRate(float *fps) const override {
*fps = 1;
return true;
}
Str fxName() const override { return "Fake2d"; }
uint8_t mFrameCounter = 0;
FixedVector<CRGB, 5> mColors;
};
TEST_CASE("test_fixed_fps") {
Fake2dPtr fake = Fake2dPtr::New();
fake->mColors.push_back(CRGB(0, 0, 0));
fake->mColors.push_back(CRGB(255, 0, 0));
VideoFxWrapper wrapper(fake);
wrapper.setFade(0, 0);
CRGB leds[1];
Fx::DrawContext context(0, leds);
wrapper.draw(context);
CHECK_EQ(1, fake->mFrameCounter);
CHECK_EQ(leds[0], CRGB(0, 0, 0));
context.now = 500;
wrapper.draw(context);
CHECK_EQ(2, fake->mFrameCounter);
CHECK_EQ(leds[0], CRGB(127, 0, 0));
}

View file

@ -0,0 +1,323 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "lib8tion/intmap.h"
#include "fl/xypath.h"
#include "fl/vector.h"
#include "fl/unused.h"
#include <string>
using namespace fl;
#define MESSAGE_TILE(TILE) \
MESSAGE("\nTile:\n" \
<< " " << TILE.at(0, 0) << " " << TILE.at(1, 0) << "\n" \
<< " " << TILE.at(0, 1) << " " << TILE.at(1, 1) << "\n");
#define MESSAGE_TILE_ROW(TILE, ROW) \
MESSAGE("\nTile Row " << ROW << ":\n" \
<< " " << TILE.at(0, ROW) << " " << TILE.at(1, ROW) << "\n");
TEST_CASE("LinePath") {
LinePath path(0.0f, 0.0f, 1.0f, 1.0f);
vec2f xy = path.compute(0.5f);
REQUIRE(xy.x == 0.5f);
REQUIRE(xy.y == 0.5f);
xy = path.compute(1.0f);
REQUIRE(xy.x == 1.0f);
REQUIRE(xy.y == 1.0f);
xy = path.compute(0.0f);
REQUIRE(xy.x == 0.0f);
REQUIRE(xy.y == 0.0f);
}
TEST_CASE("LinePath at_subpixel") {
// Tests that we can get the correct subpixel values at center point 0,0
LinePath line(-1.0f, -1.0f, 1.0f, -1.0f);
XYPath path(NewPtrNoTracking(line));
path.setDrawBounds(2,2);
Tile2x2_u8 tile = path.at_subpixel(0);
REQUIRE_EQ(vec2<uint16_t>(0, 0), tile.origin());
MESSAGE_TILE(tile);
REQUIRE_EQ(255, tile.at(0, 0));
}
TEST_CASE("LinePath simple float sweep") {
// Tests that we can get the correct gaussian values at center point 0,0
LinePath point(0, 1.f, 1.f, 1.f);
XYPath path(NewPtrNoTracking(point));
auto xy = path.at(0);
//MESSAGE_TILE(tile);
REQUIRE_EQ(xy, vec2f(0.0f, 1.f));
xy = path.at(1);
REQUIRE_EQ(xy, vec2f(1.f, 1.f));
}
TEST_CASE("Point at exactly the middle") {
// Tests that we can get the correct gaussian values at center point 0,0
PointPath point(0.0, 0.0); // Right in middle.
XYPath path(NewPtrNoTracking(point));
path.setDrawBounds(2,2);
// auto xy = path.at(0);
fl::Tile2x2_u8 sp = path.at_subpixel(0);
//MESSAGE_TILE(tile);
// REQUIRE_EQ(vec2f(0.0f, 0.f), sp);
// print out
auto origin = sp.origin();
MESSAGE("Origin: " << origin.x << ", " << origin.y);
MESSAGE(sp.at(0, 0));
MESSAGE(sp.at(0, 1));
MESSAGE(sp.at(1, 0));
MESSAGE(sp.at(1, 1));
// require that all alpha be the same
REQUIRE_EQ(sp.at(0, 0), sp.at(0, 1));
REQUIRE_EQ(sp.at(0, 0), sp.at(1, 0));
REQUIRE_EQ(sp.at(0, 0), sp.at(1, 1));
REQUIRE_EQ(sp.at(0, 0), 64);
}
TEST_CASE("LinePath simple sweep in draw bounds") {
// Tests that we can get the correct gaussian values at center point 0,0
LinePath point(-1.f, -1.f, 1.f, -1.f);
XYPath path(NewPtrNoTracking(point));
int width = 2;
path.setDrawBounds(width, width);
auto begin = path.at(0);
auto end = path.at(1);
REQUIRE_EQ(vec2f(0.5f, 0.5f), begin);
REQUIRE_EQ(vec2f(1.5f, 0.5f), end);
}
TEST_CASE("LinePath at_subpixel moves x") {
// Tests that we can get the correct subpixel.
LinePath point(-1.f, -1.f, 1.f, -1.f);
XYPath path(NewPtrNoTracking(point));
path.setDrawBounds(3, 3);
Tile2x2_u8 tile = path.at_subpixel(0.0f);
// MESSAGE_TILE(tile);
REQUIRE_EQ(tile.origin(), vec2<uint16_t>(0, 0));
REQUIRE_EQ(tile.at(0, 0), 255);
tile = path.at_subpixel(1.0f);
REQUIRE_EQ(tile.origin(), vec2<uint16_t>(2, 0));
REQUIRE_EQ(tile.at(0, 0), 255);
}
TEST_CASE("Test HeartPath") {
HeartPathPtr heart = HeartPathPtr::New();
// Track min and max values to help with scaling
float min_x = 1.0f;
float max_x = -1.0f;
float min_y = 1.0f;
float max_y = -1.0f;
// Sample points along the heart curve
const int num_samples = 100;
for (int i = 0; i < num_samples; i++) {
float alpha = static_cast<float>(i) / (num_samples - 1);
vec2f point = heart->compute(alpha);
// Update min/max values
min_x = MIN(min_x, point.x);
max_x = MAX(max_x, point.x);
min_y = MIN(min_y, point.y);
max_y = MAX(max_y, point.y);
// Print every 10th point for visual inspection
if (i % 10 == 0) {
MESSAGE("Heart point at alpha=" << alpha << ": (" << point.x << ", " << point.y << ")");
}
}
// Print the min/max values
MESSAGE("\nHeart shape bounds:");
MESSAGE("X range: [" << min_x << ", " << max_x << "]");
MESSAGE("Y range: [" << min_y << ", " << max_y << "]");
// Verify the heart is within the expected bounds
REQUIRE(min_x >= -1.0f);
REQUIRE(max_x <= 1.0f);
REQUIRE(min_y >= -1.0f);
REQUIRE(max_y <= 1.0f);
}
TEST_CASE("Test ArchimedeanSpiralPath") {
ArchimedeanSpiralPathPtr spiral = ArchimedeanSpiralPathPtr::New(3, 1.0f);
// Track min and max values to help with scaling
float min_x = 1.0f;
float max_x = -1.0f;
float min_y = 1.0f;
float max_y = -1.0f;
// Sample points along the spiral curve
const int num_samples = 100;
for (int i = 0; i < num_samples; i++) {
float alpha = static_cast<float>(i) / (num_samples - 1);
vec2f point = spiral->compute(alpha);
// Update min/max values
min_x = MIN(min_x, point.x);
max_x = MAX(max_x, point.x);
min_y = MIN(min_y, point.y);
max_y = MAX(max_y, point.y);
// Print every 10th point for visual inspection
if (i % 10 == 0) {
MESSAGE("Spiral point at alpha=" << alpha << ": (" << point.x << ", " << point.y << ")");
}
}
// Print the min/max values
MESSAGE("\nSpiral shape bounds:");
MESSAGE("X range: [" << min_x << ", " << max_x << "]");
MESSAGE("Y range: [" << min_y << ", " << max_y << "]");
// Verify the spiral is within the expected bounds
REQUIRE(min_x >= -1.0f);
REQUIRE(max_x <= 1.0f);
REQUIRE(min_y >= -1.0f);
REQUIRE(max_y <= 1.0f);
}
TEST_CASE("Test RosePath") {
// Test with different petal configurations
SUBCASE("3-petal rose") {
RosePathPtr rose = RosePathPtr::New(3, 1);
// Track min and max values to help with scaling
float min_x = 1.0f;
float max_x = -1.0f;
float min_y = 1.0f;
float max_y = -1.0f;
// Sample points along the rose curve
const int num_samples = 100;
for (int i = 0; i < num_samples; i++) {
float alpha = static_cast<float>(i) / (num_samples - 1);
vec2f point = rose->compute(alpha);
// Update min/max values
min_x = MIN(min_x, point.x);
max_x = MAX(max_x, point.x);
min_y = MIN(min_y, point.y);
max_y = MAX(max_y, point.y);
// Print every 10th point for visual inspection
if (i % 10 == 0) {
MESSAGE("3-petal rose point at alpha=" << alpha << ": (" << point.x << ", " << point.y << ")");
}
}
// Print the min/max values
MESSAGE("\n3-petal rose shape bounds:");
MESSAGE("X range: [" << min_x << ", " << max_x << "]");
MESSAGE("Y range: [" << min_y << ", " << max_y << "]");
// Verify the rose is within the expected bounds
REQUIRE(min_x >= -1.0f);
REQUIRE(max_x <= 1.0f);
REQUIRE(min_y >= -1.0f);
REQUIRE(max_y <= 1.0f);
}
SUBCASE("4-petal rose") {
RosePathPtr rose = RosePathPtr::New(2, 1); // n=2 gives 4 petals
// Track min and max values to help with scaling
float min_x = 1.0f;
float max_x = -1.0f;
float min_y = 1.0f;
float max_y = -1.0f;
// Sample points along the rose curve
const int num_samples = 100;
for (int i = 0; i < num_samples; i++) {
float alpha = static_cast<float>(i) / (num_samples - 1);
vec2f point = rose->compute(alpha);
// Update min/max values
min_x = MIN(min_x, point.x);
max_x = MAX(max_x, point.x);
min_y = MIN(min_y, point.y);
max_y = MAX(max_y, point.y);
}
// Verify the rose is within the expected bounds
REQUIRE(min_x >= -1.0f);
REQUIRE(max_x <= 1.0f);
REQUIRE(min_y >= -1.0f);
REQUIRE(max_y <= 1.0f);
}
}
TEST_CASE("Check complex types") {
HeapVector<XYPathPtr> paths;
XYPathPtr circle = XYPath::NewCirclePath();
paths.push_back(circle);
// Add heart path to the tests
XYPathPtr heart = XYPath::NewHeartPath();
paths.push_back(heart);
// Add spiral path to the tests
XYPathPtr spiral = XYPath::NewArchimedeanSpiralPath();
paths.push_back(spiral);
// Add rose path to the tests
XYPathPtr rose = XYPath::NewRosePath();
paths.push_back(rose);
// Add phyllotaxis path to the tests
XYPathPtr phyllotaxis = XYPath::NewPhyllotaxisPath();
paths.push_back(phyllotaxis);
// paths.push_back(LissajousPathPtr::New());
// paths.push_back(GielisCurvePathPtr::New());
// paths.push_back(CatmullRomPathPtr::New());
SUBCASE("Check floating point range") {
for (auto &path : paths) {
for (float alpha = 0.0f; true; alpha += 0.01f) {
alpha = MIN(1.f, alpha);
vec2f xy = path->at(alpha);
REQUIRE(xy.x >= -1.0f);
REQUIRE(xy.x <= 1.0f);
REQUIRE(xy.y >= -1.0f);
REQUIRE(xy.y <= 1.0f);
if (ALMOST_EQUAL(alpha, 1.0f, 0.001f)) {
break;
}
}
}
}
SUBCASE("Check float point range with transform to -8,8") {
TransformFloat tx;
tx.set_scale(4.0f);
for (auto &path : paths) {
for (float alpha = 0.0f; true; alpha += 0.01f) {
alpha = MIN(1.f, alpha);
vec2f xy = path->at(alpha, tx);
REQUIRE_GE(xy.x, -4.0f);
REQUIRE_LE(xy.x, 4.0f);
REQUIRE_GE(xy.y, -4.0f);
REQUIRE_LE(xy.y, 4.0f);
if (ALMOST_EQUAL(alpha, 1.0f, 0.001f)) {
break;
}
}
}
}
}