Initial commit of Arduino libraries

This commit is contained in:
Sam
2025-05-23 10:47:41 +10:00
commit 5bfce5fc3e
2476 changed files with 1108481 additions and 0 deletions

View File

@@ -0,0 +1,230 @@
# 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)
# 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=suggest-override
-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=non-virtual-dtor
-Werror=reorder
-Werror=sign-compare
-Werror=float-equal
#-Werror=conversion
-Werror=switch-enum
#-Werror=switch-default
-Werror=unused-parameter
-Werror=unused-variable
-Werror=unused-value
-Werror=cast-align
-DFASTLED_FIVE_BIT_HD_GAMMA_FUNCTION_2_8
# ignore Arduino/PlatformIO-specific PROGMEM macro
-DPROGMEM=
)
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND COMMON_COMPILE_FLAGS -Werror=self-assign -Werror=infinite-recursion -Werror=extra-tokens)
endif()
set(UNIT_TEST_COMPILE_FLAGS
-Wall
#-Wextra
#-Wpedantic
-funwind-tables
-g3
-ggdb
-fno-omit-frame-pointer
-O0
-Werror=suggest-override
-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=non-virtual-dtor
#-Werror=reorder
#-Werror=sign-compare
#-Werror=float-equal
#-Werror=conversion
-Werror=switch-enum
#-Werror=switch-default
#-Werror=unused-parameter
#-Werror=unused-variable
#-Werror=unused-value
# Not supported in gcc.
#-Werror=infinite-recursion
#-v
)
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()
# 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)
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)
target_link_options(${TEST_NAME} PRIVATE -static-libgcc -static-libstdc++)
endif()
target_compile_options(${TEST_NAME} PRIVATE ${UNIT_TEST_COMPILE_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

7106
FastLED/tests/doctest.h Normal file

File diff suppressed because it is too large Load Diff

3
FastLED/tests/readme Normal file
View File

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

24
FastLED/tests/test.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include "crgb.h"
#include "fl/str.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();
}
};
}

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,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,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,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,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
SetLargeBlockAllocator(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") {
SetLargeBlockAllocator(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_BLACK);
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_BLACK);
CHECK(out == CRGB(64, 0, 0));
}

View File

@@ -0,0 +1,284 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fx/frame.h"
#include "fx/video/frame_interpolator.h"
#include "fl/namespace.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
#if 0
TEST_CASE("FrameInterpolator::selectFrames") {
SUBCASE("Empty interpolator") {
FrameInterpolator interpolator(5, -1);
const Frame *selected1;
const Frame *selected2;
CHECK_FALSE(interpolator.selectFrames(0, &selected1, &selected2));
}
SUBCASE("2 frame interpolator before") {
// Create an interpolator with capacity for 2 frames
FrameInterpolator interpolator(2, -1);
// Create some test frames with different timestamps
FrameRef frame1 = FrameRef::New(10, false); // 10 pixels, no alpha
FrameRef frame2 = FrameRef::New(10, false);
// Add frames with timestamps
CHECK(interpolator.push_front(frame1, 0, 1000));
CHECK(interpolator.push_front(frame2, 1, 2000));
const Frame *selected1;
const Frame *selected2;
// Falls between two frames->
bool selected = interpolator.selectFrames(0, &selected1, &selected2);
CHECK(selected);
CHECK(selected1);
CHECK(selected2);
// Now check that it's the same frame.
CHECK(selected1 == selected2);
// now check that the timestamp of the first frame is less than the
// timestamp of the second frame
CHECK(selected1->getTimestamp() == 1000);
CHECK(selected2->getTimestamp() == 1000);
}
SUBCASE("2 frame interpolator between") {
// Create an interpolator with capacity for 2 frames
FrameInterpolator interpolator(2, -1);
// Create some test frames with different timestamps
FrameRef frame1 = FrameRef::New(10, false); // 10 pixels, no alpha
FrameRef frame2 = FrameRef::New(10, false);
// Add frames with timestamps
//CHECK(interpolator.addWithTimestamp(frame1, 0));
//CHECK(interpolator.addWithTimestamp(frame2, 1000));
CHECK(interpolator.push_front(frame1, 0, 1000));
CHECK(interpolator.push_front(frame2, 1, 2000));
const Frame *selected1;
const Frame *selected2;
// Falls between two frames->
bool selected = interpolator.selectFrames(500, &selected1, &selected2);
CHECK(selected);
CHECK(selected1);
CHECK(selected2);
// now check that the frames are different
CHECK(selected1 != selected2);
// now check that the timestamp of the first frame is less than the
// timestamp of the second frame
CHECK(selected1->getTimestamp() == 0);
CHECK(selected2->getTimestamp() == 1000);
}
SUBCASE("2 frame interpolator after") {
// Create an interpolator with capacity for 2 frames
FrameInterpolator interpolator(2, -1);
// Create some test frames with different timestamps
FrameRef frame1 = FrameRef::New(10, false); // 10 pixels, no alpha
FrameRef frame2 = FrameRef::New(10, false);
// Add frames with timestamps
CHECK(interpolator.push_front(frame1, 0, 0));
CHECK(interpolator.push_front(frame2, 1, 1000));
const Frame *selected1;
const Frame *selected2;
// Falls between two frames->
bool selected = interpolator.selectFrames(1500, &selected1, &selected2);
CHECK(selected);
CHECK(selected1);
CHECK(selected2);
// now check that the frames are different
CHECK(selected1 == selected2);
// now check that the timestamp of the first frame is less than the
// timestamp of the second frame
CHECK(selected1->getTimestamp() == 1000);
CHECK(selected2->getTimestamp() == 1000);
}
}
TEST_CASE("FrameInterpolator::addWithTimestamp") {
SUBCASE("add first frame") {
FrameInterpolator interpolator(5, -1);
FrameRef frame = FrameRef::New(10, false);
CHECK(interpolator.push_front(frame, 0, 1000));
FrameInterpolator::FrameBuffer* frames = interpolator.getFrames();
CHECK_EQ(frames->size(), 1);
CHECK_EQ(frames->front().frame->getTimestamp(), 1000);
}
SUBCASE("add second frame which is before first frame and should be rejected") {
FrameInterpolator interpolator(5, -1);
FrameRef frame1 = FrameRef::New(10, false);
FrameRef frame2 = FrameRef::New(10, false);
CHECK(interpolator.push_front(frame1, 0, 1000));
CHECK_FALSE(interpolator.push_front(frame2, 1, 500));
FrameInterpolator::FrameBuffer* frames = interpolator.getFrames();
CHECK_EQ(frames->size(), 1);
CHECK_EQ(frames->front().frame->getTimestamp(), 1000);
}
SUBCASE("add second frame which has the same timestamp as first frame and should be rejected") {
FrameInterpolator interpolator(5, -1);
FrameRef frame1 = FrameRef::New(10, false);
FrameRef frame2 = FrameRef::New(10, false);
CHECK(interpolator.push_front(frame1, 0, 1000));
CHECK_FALSE(interpolator.push_front(frame2, 1, 1000));
FrameInterpolator::FrameBuffer* frames = interpolator.getFrames();
CHECK_EQ(frames->size(), 1);
CHECK_EQ(frames->front().frame->getTimestamp(), 1000);
}
SUBCASE("add second frame which is after first frame and should be accepted") {
FrameInterpolator interpolator(5, -1);
FrameRef frame1 = FrameRef::New(10, false);
FrameRef frame2 = FrameRef::New(10, false);
CHECK(interpolator.push_front(frame1, 0, 1000));
CHECK(interpolator.push_front(frame2, 1, 1500));
FrameInterpolator::FrameBuffer* frames = interpolator.getFrames();
CHECK_EQ(frames->size(), 2);
CHECK_EQ(frames->front().frame->getTimestamp(), 1500);
CHECK_EQ(frames->back().frame->getTimestamp(), 1000);
}
}
TEST_CASE("FrameInterpolator::addWithTimestamp and overflow") {
SUBCASE("add two frames and check time") {
FrameInterpolator interpolator(2, -1);
FrameRef frame = FrameRef::New(10, false);
CHECK(interpolator.push_front(frame, 0, 1000));
CHECK(interpolator.push_front(frame, 1, 2000));
CHECK(interpolator.push_front(frame, 2, 3000));
FrameInterpolator::FrameBuffer* frames = interpolator.getFrames();
CHECK_EQ(frames->size(), 2);
CHECK_EQ(frames->front().frame->getTimestamp(), 3000);
CHECK_EQ(frames->back().frame->getTimestamp(), 2000);
}
SUBCASE("add two frames and check that Frame object was recycled") {
FrameInterpolator interpolator(2, -1);
FrameInterpolator::FrameBuffer* frames = interpolator.getFrames();
CHECK_EQ(2, frames->capacity());
CHECK_EQ(0, frames->size());
FrameRef frame = FrameRef::New(2, false);
CHECK(interpolator.push_front(frame, 0, 1000));
CHECK_EQ(2, frames->capacity());
CHECK_EQ(1, frames->size());
CHECK(interpolator.push_front(frame, 1, 2000));
CHECK_EQ(2, frames->capacity());
CHECK_EQ(2, frames->size());
CHECK(frames->full());
Frame* frameThatShouldBeRecycled = frames->back().frame.get();
CHECK(interpolator.push_front(frame, 2, 3000));
CHECK_EQ(frames->size(), 2);
CHECK_EQ(frames->front().frame->getTimestamp(), 3000);
// check pointers are equal
CHECK(frames->front().frame == frameThatShouldBeRecycled);
}
}
TEST_CASE("FrameInterpolator::draw") {
SUBCASE("Empty interpolator") {
FrameInterpolator interpolator(5, -1);
FrameRef frame = FrameRef::New(10, false);
FrameRef dst = FrameRef::New(10, false);
CHECK_FALSE(interpolator.draw(0, dst.get()));
}
SUBCASE("Add one frame and check that we will draw with that") {
FrameInterpolator interpolator(5, -1);
FrameRef frame = FrameRef::New(10, false);
CHECK(interpolator.push_front(frame, 0, 1000));
FrameRef dst = FrameRef::New(10, false);
CHECK(interpolator.draw(0, dst.get()));
CHECK_EQ(dst->getTimestamp(), 1000);
CHECK(interpolator.draw(2000, dst.get()));
CHECK_EQ(dst->getTimestamp(), 1000);
}
SUBCASE("Add two frames and check behavior for drawing before, between and after") {
FrameInterpolator interpolator(5, -1);
FrameRef frame1 = FrameRef::New(10, false);
FrameRef frame2 = FrameRef::New(10, false);
CHECK(interpolator.push_front(frame1, 0, 1000));
CHECK(interpolator.push_front(frame2, 1, 2000));
FrameRef dst = FrameRef::New(10, false);
CHECK(interpolator.draw(0, dst.get()));
CHECK_EQ(dst->getTimestamp(), 1000);
CHECK(interpolator.draw(1500, dst.get()));
CHECK_EQ(dst->getTimestamp(), 1500);
CHECK(interpolator.draw(2500, dst.get()));
CHECK_EQ(dst->getTimestamp(), 2000);
}
SUBCASE("Add three frames and check behavior for drawing before, between 0&1, between 1&2 and after") {
FrameInterpolator interpolator(5, -1);
FrameRef frame1 = FrameRef::New(10, false);
FrameRef frame2 = FrameRef::New(10, false);
FrameRef frame3 = FrameRef::New(10, false);
CHECK(interpolator.push_front(frame1, 0, 1000));
CHECK(interpolator.push_front(frame2, 1, 2000));
CHECK(interpolator.push_front(frame3, 2, 3000));
FrameRef dst = FrameRef::New(10, false);
CHECK(interpolator.draw(0, dst.get()));
CHECK_EQ(dst->getTimestamp(), 1000);
// now check what happens when we draw exactly at 1000
CHECK(interpolator.draw(1000, dst.get()));
CHECK_EQ(dst->getTimestamp(), 1000);
CHECK(interpolator.draw(1500, dst.get()));
CHECK_EQ(dst->getTimestamp(), 1500);
// now check what happens when we draw exactly at 2000
CHECK(interpolator.draw(2000, dst.get()));
CHECK_EQ(dst->getTimestamp(), 2000);
CHECK(interpolator.draw(2500, dst.get()));
CHECK_EQ(dst->getTimestamp(), 2500);
CHECK(interpolator.draw(3500, dst.get()));
CHECK_EQ(dst->getTimestamp(), 3000);
}
SUBCASE("Check that the draw command interpolates between two added frames when queried from the middle") {
FrameInterpolator interpolator(5, -1);
FrameRef frame1 = FrameRef::New(10, false);
FrameRef frame2 = FrameRef::New(10, false);
// frame 1 is all red
for (int i = 0; i < 10; i++) {
frame1->rgb()[i] = CRGB::Red;
}
// frame 2 is all blue
for (int i = 0; i < 10; i++) {
frame2->rgb()[i] = CRGB::Blue;
}
CHECK(interpolator.push_front(frame1, 0, 1000));
CHECK(interpolator.push_front(frame2, 1, 2000));
FrameRef dst = FrameRef::New(10, false);
CHECK(interpolator.draw(1500, dst.get()));
CHECK_EQ(dst->getTimestamp(), 1500);
// now check that the frame is interpolated between red and blue
for (int i = 0; i < 10; i++) {
CHECK(dst->rgb()[i] == CRGB(128, 0, 127));
}
}
}
#endif

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);
}

32
FastLED/tests/test_fx.cpp Normal file
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("TimeScale basic functionality") {
FASTLED_USING_NAMESPACE;
SUBCASE("Initialization and normal time progression") {
TimeScale 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") {
TimeScale tw(1000);
tw.setScale(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.setScale(0.5f); // Set half speed: 500ms.
CHECK(tw.scale() == 0.5f);
tw.update(2500);
CHECK(tw.time() == 1500);
}
SUBCASE("Reset functionality") {
TimeScale 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") {
TimeScale tw(1000, 1.0f);
tw.update(1001);
CHECK(tw.time() == 1);
tw.setScale(-1.0f);
tw.update(2000);
CHECK_EQ(tw.time(), 0);
}
}

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);
}

139
FastLED/tests/test_map.cpp Normal file
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,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,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,226 @@
// 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() override { Referent::ref(); }
virtual void unref() override { Referent::unref(); }
virtual void destroy() 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());
}
}

View File

@@ -0,0 +1,128 @@
// 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);
}

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,14 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#include "test.h"
#include "fl/slice.h"
#include "fl/namespace.h"
FASTLED_USING_NAMESPACE
TEST_CASE("compile test") {
}

147
FastLED/tests/test_str.cpp Normal file
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,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.update(t0) == 0);
// midrise: 5 ms → (5*255/10) ≈ 127
REQUIRE(ramp.update(t0 + 5) == static_cast<uint8_t>((5 * 255) / 10));
// end of rise: 10 ms → full on
REQUIRE(ramp.update(t0 + 10) == 255);
// plateau: well within [10, 90) ms
REQUIRE(ramp.update(t0 + 50) == 255);
// midfall: elapsed=95 ms → fallingElapsed=5 ms → 255 - (5*255/10) ≈ 128
uint8_t value = ramp.update(t0 + 115);
uint8_t expected = static_cast<uint8_t>(255 - (5 * 255) / 10);
REQUIRE_EQ(expected, value);
// after latch: 110 ms → off
// REQUIRE(ramp.update(t0 + 110) == 0);
// now do it again
ramp.trigger(200);
// at start: still at zero
REQUIRE(ramp.update(200) == 0);
// midrise: 205 ms → (5*255/10) ≈ 127
REQUIRE(ramp.update(205) == static_cast<uint8_t>((5 * 255) / 10));
// end of rise: 210 ms → full on
REQUIRE(ramp.update(210) == 255);
// plateau: well within [210, 290) ms
REQUIRE(ramp.update(250) == 255);
// midfall: elapsed=295 ms → fallingElapsed=5 ms → 255 - (5*255/10) ≈ 128
REQUIRE(ramp.update(315) == static_cast<uint8_t>(255 - (5 * 255) / 10));
// after latch: 310 ms → off
REQUIRE(ramp.update(320) == 0);
// after latch: 410 ms → off
REQUIRE(ramp.update(410) == 0);
}
TEST_CASE("Real world Bug") {
TimeRamp transition = TimeRamp(500, 0, 500);
uint8_t value = transition.update(0);
CHECK(value == 0);
value = transition.update(1);
CHECK(value == 0);
transition.trigger(6900);
value = transition.update(6900);
REQUIRE_EQ(value, 0);
value = transition.update(6900 + 500);
REQUIRE_EQ(value, 255);
value = transition.update(6900 + 250);
REQUIRE_EQ(value, 127);
}

15
FastLED/tests/test_ui.cpp Normal file
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,390 @@
// g++ --std=c++11 test.cpp
#include "test.h"
#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);
}
}
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));
}