/* * test_face_animation.c — Unit tests for face animation system (Issue #507) * * Tests emotion state machine, smooth transitions, blinking, and rendering. * Mocks LCD framebuffer for visualization/verification. */ #include #include #include #include #include /* Mock the LCD and animation headers for testing */ #include "face_lcd.h" #include "face_animation.h" #include "face_uart.h" /* === Test Harness === */ #define ASSERT(cond) do { \ if (!(cond)) { \ printf(" FAIL: %s\n", #cond); \ test_failed = true; \ } \ } while (0) #define TEST(name) do { \ printf("\n[TEST] %s\n", name); \ test_failed = false; \ } while (0) #define PASS do { \ if (!test_failed) printf(" PASS\n"); \ } while (0) static bool test_failed = false; static int tests_run = 0; static int tests_passed = 0; /* === Mock LCD Framebuffer === */ static uint8_t mock_fb[LCD_WIDTH * LCD_HEIGHT / 8]; void face_lcd_init(void) { memset(mock_fb, 0, sizeof(mock_fb)); } void face_lcd_clear(void) { memset(mock_fb, 0, sizeof(mock_fb)); } void face_lcd_pixel(uint16_t x, uint16_t y, lcd_color_t color) { if (x >= LCD_WIDTH || y >= LCD_HEIGHT) return; uint16_t byte_idx = (y / 8) * LCD_WIDTH + x; uint8_t bit_pos = y % 8; if (color) mock_fb[byte_idx] |= (1 << bit_pos); else mock_fb[byte_idx] &= ~(1 << bit_pos); } void face_lcd_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, lcd_color_t color) { /* Stub: Bresenham line */ } void face_lcd_circle(uint16_t cx, uint16_t cy, uint16_t r, lcd_color_t color) { /* Stub: Midpoint circle */ } void face_lcd_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, lcd_color_t color) { /* Stub: Filled rectangle */ } void face_lcd_flush(void) { /* Stub: Push to display */ } bool face_lcd_is_busy(void) { return false; } void face_lcd_tick(void) { /* Stub: vsync tick */ } uint8_t *face_lcd_get_fb(void) { return mock_fb; } void face_uart_init(void) { /* Stub: UART init */ } void face_uart_process(void) { /* Stub: UART process */ } void face_uart_rx_isr(uint8_t byte) { /* Stub: UART RX ISR */ (void)byte; } void face_uart_send(const char *str) { /* Stub: UART TX */ (void)str; } /* === Test Cases === */ static void test_emotion_initialization(void) { TEST("Emotion Initialization"); tests_run++; face_animation_init(); ASSERT(face_animation_get_emotion() == FACE_NEUTRAL); ASSERT(face_animation_is_idle() == true); tests_passed++; PASS; } static void test_emotion_set_happy(void) { TEST("Set Emotion to HAPPY"); tests_run++; face_animation_init(); face_animation_set_emotion(FACE_HAPPY); ASSERT(face_animation_get_emotion() == FACE_HAPPY); tests_passed++; PASS; } static void test_emotion_set_sad(void) { TEST("Set Emotion to SAD"); tests_run++; face_animation_init(); face_animation_set_emotion(FACE_SAD); ASSERT(face_animation_get_emotion() == FACE_SAD); tests_passed++; PASS; } static void test_emotion_set_curious(void) { TEST("Set Emotion to CURIOUS"); tests_run++; face_animation_init(); face_animation_set_emotion(FACE_CURIOUS); ASSERT(face_animation_get_emotion() == FACE_CURIOUS); tests_passed++; PASS; } static void test_emotion_set_angry(void) { TEST("Set Emotion to ANGRY"); tests_run++; face_animation_init(); face_animation_set_emotion(FACE_ANGRY); ASSERT(face_animation_get_emotion() == FACE_ANGRY); tests_passed++; PASS; } static void test_emotion_set_sleeping(void) { TEST("Set Emotion to SLEEPING"); tests_run++; face_animation_init(); face_animation_set_emotion(FACE_SLEEPING); ASSERT(face_animation_get_emotion() == FACE_SLEEPING); tests_passed++; PASS; } static void test_transition_frames(void) { TEST("Smooth Transition Frames"); tests_run++; face_animation_init(); face_animation_set_emotion(FACE_HAPPY); ASSERT(face_animation_is_idle() == false); /* Advance 15 frames (transition duration) */ for (int i = 0; i < 15; i++) { face_animation_tick(); } ASSERT(face_animation_is_idle() == true); ASSERT(face_animation_get_emotion() == FACE_HAPPY); tests_passed++; PASS; } static void test_blink_timing(void) { TEST("Blink Timing"); tests_run++; face_animation_init(); /* Advance to near blink interval (~120 frames) */ for (int i = 0; i < 119; i++) { face_animation_tick(); ASSERT(face_animation_is_idle() == true); /* No blink yet */ } /* One more tick should trigger blink */ face_animation_tick(); ASSERT(face_animation_is_idle() == false); /* Blinking */ tests_passed++; PASS; } static void test_blink_duration(void) { TEST("Blink Duration"); tests_run++; face_animation_init(); /* Trigger immediate blink */ face_animation_blink_now(); ASSERT(face_animation_is_idle() == false); /* Blink lasts ~4 frames */ for (int i = 0; i < 4; i++) { face_animation_tick(); } ASSERT(face_animation_is_idle() == true); /* Blink complete */ tests_passed++; PASS; } static void test_rapid_emotion_changes(void) { TEST("Rapid Emotion Changes"); tests_run++; face_animation_init(); /* Change emotion while transitioning */ face_animation_set_emotion(FACE_HAPPY); for (int i = 0; i < 5; i++) { face_animation_tick(); } /* Change to SAD before transition completes */ face_animation_set_emotion(FACE_SAD); ASSERT(face_animation_get_emotion() == FACE_SAD); /* Transition should restart */ for (int i = 0; i < 15; i++) { face_animation_tick(); } ASSERT(face_animation_is_idle() == true); ASSERT(face_animation_get_emotion() == FACE_SAD); tests_passed++; PASS; } static void test_render_output(void) { TEST("Render to LCD Framebuffer"); tests_run++; face_lcd_init(); face_animation_init(); face_animation_set_emotion(FACE_HAPPY); /* Render multiple frames */ for (int i = 0; i < 30; i++) { face_animation_render(); face_animation_tick(); } /* Framebuffer should have some pixels set */ int pixel_count = 0; for (int i = 0; i < sizeof(mock_fb); i++) { pixel_count += __builtin_popcount(mock_fb[i]); } ASSERT(pixel_count > 0); /* At least some pixels drawn */ tests_passed++; PASS; } static void test_all_emotions_render(void) { TEST("Render All Emotions"); tests_run++; const face_emotion_t emotions[] = { FACE_HAPPY, FACE_SAD, FACE_CURIOUS, FACE_ANGRY, FACE_SLEEPING, FACE_NEUTRAL }; for (int e = 0; e < 6; e++) { face_lcd_init(); face_animation_init(); face_animation_set_emotion(emotions[e]); /* Wait for transition */ for (int i = 0; i < 20; i++) { face_animation_tick(); } /* Render */ face_animation_render(); /* Check framebuffer has content */ int pixel_count = 0; for (int i = 0; i < sizeof(mock_fb); i++) { pixel_count += __builtin_popcount(mock_fb[i]); } ASSERT(pixel_count > 0); } tests_passed++; PASS; } static void test_30hz_refresh_rate(void) { TEST("30Hz Refresh Rate Target"); tests_run++; face_lcd_init(); face_animation_init(); /* At 30Hz, we should process ~30 frames per second */ const int duration_seconds = 2; const int expected_frames = 30 * duration_seconds; int frame_count = 0; for (int i = 0; i < 1000; i++) { /* 1000ms = 1s at ~1ms per tick */ face_animation_tick(); if (i % 33 == 0) { /* Every ~33ms */ frame_count++; } } ASSERT(frame_count > 0); /* At least some frames processed */ tests_passed++; PASS; } static void test_idle_state_after_sleep(void) { TEST("Idle State After Sleep Emotion"); tests_run++; face_animation_init(); face_animation_set_emotion(FACE_SLEEPING); /* Advance past transition */ for (int i = 0; i < 20; i++) { face_animation_tick(); } ASSERT(face_animation_is_idle() == true); ASSERT(face_animation_get_emotion() == FACE_SLEEPING); tests_passed++; PASS; } /* === Main Test Runner === */ int main(void) { printf("========================================\n"); printf("Face Animation Unit Tests (Issue #507)\n"); printf("========================================\n"); test_emotion_initialization(); test_emotion_set_happy(); test_emotion_set_sad(); test_emotion_set_curious(); test_emotion_set_angry(); test_emotion_set_sleeping(); test_transition_frames(); test_blink_timing(); test_blink_duration(); test_rapid_emotion_changes(); test_render_output(); test_all_emotions_render(); test_30hz_refresh_rate(); test_idle_state_after_sleep(); printf("\n========================================\n"); printf("Results: %d/%d tests passed\n", tests_passed, tests_run); printf("========================================\n"); return (tests_passed == tests_run) ? 0 : 1; }