Files
jack-looper/test_tui.c
Loic Coenen eba0d3f05f (no commit message provided)
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
2026-05-05 10:08:06 +00:00

1575 lines
46 KiB
C

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <stdbool.h>
#include "dispatcher.h"
#include "tui.h"
// Test helper functions
static AppState* create_test_state(void) {
AppState *state = (AppState *)calloc(1, sizeof(AppState));
assert(state != NULL);
// Initialize clips
for (int i = 0; i < MAX_CLIPS; i++) {
state->clips[i].state = CLIP_EMPTY;
state->clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float));
assert(state->clips[i].buffer != NULL);
state->clips[i].buffer_size = 0;
state->clips[i].write_position = 0;
state->clips[i].read_position = 0;
}
// Initialize transport
state->transport_state = TRANSPORT_STOPPED;
state->clock_source = CLOCK_SOURCE_INTERNAL;
state->bpm = 120.0;
state->samples_per_beat = (48000 * 60.0) / 120.0;
state->clock_count = 0;
state->beat_position = 0;
state->bar_position = 0;
state->sample_position = 0;
state->sample_accumulator = 0.0;
// Initialize quantize
state->quantize_mode = QUANTIZE_OFF;
state->quantize_threshold = 0;
// Initialize undo
state->undo.undo_index = 0;
state->undo.redo_index = 0;
state->undo.count = 0;
state->undo.current_batch_size = 0;
for (int i = 0; i < MAX_UNDO_HISTORY; i++) {
state->undo.prev_clip_indices[i] = -1;
state->undo.batch_sizes[i] = 0;
}
// JACK info
state->sample_rate = 48000;
state->running = true;
return state;
}
static void destroy_test_state(AppState *state) {
if (state) {
for (int i = 0; i < MAX_CLIPS; i++) {
free(state->clips[i].buffer);
state->clips[i].buffer = NULL;
}
free(state);
}
}
// Test 1: Grid to clip index mapping
void test_grid_to_clip_index(void) {
printf("Test 1: Grid to clip index mapping... ");
// 8x8 grid should map to 64 clips
assert(0 * 8 + 0 == 0); // Top-left
assert(0 * 8 + 7 == 7); // Top-right
assert(7 * 8 + 0 == 56); // Bottom-left
assert(7 * 8 + 7 == 63); // Bottom-right
assert(3 * 8 + 4 == 28); // Middle
printf("PASSED\n");
}
// Test 2: Trigger clip via grid position
void test_trigger_via_grid(void) {
printf("Test 2: Trigger clip via grid position... ");
AppState *state = create_test_state();
// Simulate pressing 't' on grid position (3, 4) = clip 28
int clip_idx = 3 * 8 + 4;
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx };
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 3: Reset clip via grid position
void test_reset_via_grid(void) {
printf("Test 3: Reset clip via grid position... ");
AppState *state = create_test_state();
// Set up a clip at grid position (1, 2) = clip 10
int clip_idx = 1 * 8 + 2;
state->clips[clip_idx].state = CLIP_LOOPING;
state->clips[clip_idx].buffer_size = 100;
// Simulate pressing 'r'
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clip_idx };
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_EMPTY);
assert(state->clips[clip_idx].buffer_size == 0);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 4: Scene trigger via grid row
void test_scene_via_grid(void) {
printf("Test 4: Scene trigger via grid row... ");
AppState *state = create_test_state();
// Simulate pressing 's' on row 3
int scene_index = 3;
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = scene_index };
reducer(state, action);
// All clips in scene 3 should be recording
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(scene_index, ch);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 5: Quantize mode cycling
void test_quantize_cycling(void) {
printf("Test 5: Quantize mode cycling... ");
AppState *state = create_test_state();
// Simulate pressing 'q' to cycle through modes
assert(state->quantize_mode == QUANTIZE_OFF);
// Cycle: OFF -> BEAT
Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode.mode = QUANTIZE_BEAT };
reducer(state, action);
assert(state->quantize_mode == QUANTIZE_BEAT);
// Cycle: BEAT -> BAR
action.data.set_quantize_mode.mode = QUANTIZE_BAR;
reducer(state, action);
assert(state->quantize_mode == QUANTIZE_BAR);
// Cycle: BAR -> OFF
action.data.set_quantize_mode.mode = QUANTIZE_OFF;
reducer(state, action);
assert(state->quantize_mode == QUANTIZE_OFF);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 6: Threshold toggling
void test_threshold_toggle(void) {
printf("Test 6: Threshold toggling... ");
AppState *state = create_test_state();
// Simulate pressing 'T' to toggle threshold
assert(state->quantize_threshold == 0);
// Toggle to 1000
Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold.threshold = 1000 };
reducer(state, action);
assert(state->quantize_threshold == 1000);
// Toggle back to 0
action.data.set_quantize_threshold.threshold = 0;
reducer(state, action);
assert(state->quantize_threshold == 0);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 7: Transport reset
void test_transport_reset_via_tui(void) {
printf("Test 7: Transport reset via TUI... ");
AppState *state = create_test_state();
// Set up transport state
state->transport_state = TRANSPORT_PLAYING;
state->clock_count = 100;
state->beat_position = 2;
state->bar_position = 5;
state->sample_position = 10000;
// Simulate pressing 'x'
Action action = { .type = ACTION_TRANSPORT_STOP };
reducer(state, action);
assert(state->transport_state == TRANSPORT_STOPPED);
assert(state->clock_count == 0);
assert(state->beat_position == 0);
assert(state->bar_position == 0);
assert(state->sample_position == 0);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 8: Navigation wrapping
void test_navigation_wrapping(void) {
printf("Test 8: Navigation wrapping... ");
// Test that navigation wraps around the grid
// Left from column 0 should go to column 7
int col = 0;
col = (col - 1 + 8) % 8;
assert(col == 7);
// Right from column 7 should go to column 0
col = 7;
col = (col + 1) % 8;
assert(col == 0);
// Up from row 0 should go to row 7
int row = 0;
row = (row - 1 + 8) % 8;
assert(row == 7);
// Down from row 7 should go to row 0
row = 7;
row = (row + 1) % 8;
assert(row == 0);
printf("PASSED\n");
}
// Test 9: Multiple clips in different states
void test_multiple_clip_states(void) {
printf("Test 9: Multiple clips in different states... ");
AppState *state = create_test_state();
// Set up clips in various states
state->clips[0].state = CLIP_EMPTY;
state->clips[1].state = CLIP_RECORDING;
state->clips[2].state = CLIP_LOOPING;
state->clips[3].state = CLIP_STOPPED;
// Verify states
assert(state->clips[0].state == CLIP_EMPTY);
assert(state->clips[1].state == CLIP_RECORDING);
assert(state->clips[2].state == CLIP_LOOPING);
assert(state->clips[3].state == CLIP_STOPPED);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 10: Buffer size display
void test_buffer_size_display(void) {
printf("Test 10: Buffer size display... ");
AppState *state = create_test_state();
// Set up a clip with known buffer size
state->clips[5].state = CLIP_LOOPING;
state->clips[5].buffer_size = 48000; // 1 second at 48kHz
// Verify buffer size
assert(state->clips[5].buffer_size == 48000);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 11: Help toggle
void test_help_toggle(void) {
printf("Test 11: Help toggle... ");
// Test that help flag toggles correctly
bool show_help = false;
show_help = !show_help;
assert(show_help == true);
show_help = !show_help;
assert(show_help == false);
printf("PASSED\n");
}
// Test 12: Escape key handling
void test_escape_handling(void) {
printf("Test 12: Escape key handling... ");
// Test that escape key (27) is handled
int ch = 27;
assert(ch == 27); // Escape
// Test that 'Q' is handled
ch = 'Q';
assert(ch == 'Q');
printf("PASSED\n");
}
// Test 13: TUI init and cleanup (without ncurses)
void test_tui_init_cleanup(void) {
printf("Test 13: TUI init and cleanup... ");
AppState *state = create_test_state();
// Verify state is valid
assert(state->sample_rate == 48000);
assert(state->running == true);;
destroy_test_state(state);
printf("PASSED (skipped ncurses init)\n");
}
// Test 14: State to color mapping
void test_state_to_color_mapping(void) {
printf("Test 14: State to color mapping... ");
// Verify state values match expected color indices
assert(CLIP_EMPTY == 0);
assert(CLIP_RECORDING == 1);
assert(CLIP_LOOPING == 2);
assert(CLIP_STOPPED == 3);
printf("PASSED\n");
}
// Test 15: Full grid coverage
void test_full_grid_coverage(void) {
printf("Test 15: Full grid coverage... ");
AppState *state = create_test_state();
// Trigger all 64 clips via grid positions
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
int clip_idx = row * 8 + col;
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx };
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
}
}
// Verify all clips are recording
for (int i = 0; i < 64; i++) { // Only check the 8x8 grid clips
assert(state->clips[i].state == CLIP_RECORDING);
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 16: Scene trigger from each row
void test_scene_from_each_row(void) {
printf("Test 16: Scene trigger from each row... ");
AppState *state = create_test_state();
// Trigger scene from each row
for (int row = 0; row < 8; row++) {
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = row };
reducer(state, action);
// Verify all clips in this scene are recording
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(row, ch);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
}
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 17: Quantize mode cycle through all modes
void test_quantize_full_cycle(void) {
printf("Test 17: Quantize mode full cycle... ");
AppState *state = create_test_state();
// Cycle through all modes twice
for (int cycle = 0; cycle < 2; cycle++) {
Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode.mode = QUANTIZE_OFF };
reducer(state, action);
assert(state->quantize_mode == QUANTIZE_OFF);
action.data.set_quantize_mode.mode = QUANTIZE_BEAT;
reducer(state, action);
assert(state->quantize_mode == QUANTIZE_BEAT);
action.data.set_quantize_mode.mode = QUANTIZE_BAR;
reducer(state, action);
assert(state->quantize_mode == QUANTIZE_BAR);
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 18: Multiple threshold toggles
void test_multiple_threshold_toggles(void) {
printf("Test 18: Multiple threshold toggles... ");
AppState *state = create_test_state();
// Toggle threshold multiple times
for (int i = 0; i < 5; i++) {
if (state->quantize_threshold == 0) {
Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold.threshold = 1000 };
reducer(state, action);
assert(state->quantize_threshold == 1000);
} else {
Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold.threshold = 0 };
reducer(state, action);
assert(state->quantize_threshold == 0);
}
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 19: Transport reset multiple times
void test_multiple_transport_resets(void) {
printf("Test 19: Multiple transport resets... ");
AppState *state = create_test_state();
// Reset transport multiple times
for (int i = 0; i < 5; i++) {
state->transport_state = TRANSPORT_PLAYING;
state->clock_count = 100 + i;
state->beat_position = i % 4;
state->bar_position = i;
state->sample_position = 10000 * i;
Action action = { .type = ACTION_TRANSPORT_STOP };
reducer(state, action);
assert(state->transport_state == TRANSPORT_STOPPED);
assert(state->clock_count == 0);
assert(state->beat_position == 0);
assert(state->bar_position == 0);
assert(state->sample_position == 0);
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 20: Navigation with arrow keys
void test_arrow_key_navigation(void) {
printf("Test 20: Arrow key navigation... ");
// Test that arrow keys produce same results as hjkl
int row = 3, col = 4;
// KEY_LEFT (same as 'h')
col = (col - 1 + 8) % 8;
assert(col == 3);
// KEY_DOWN (same as 'j')
row = (row + 1) % 8;
assert(row == 4);
// KEY_UP (same as 'k')
row = (row - 1 + 8) % 8;
assert(row == 3);
// KEY_RIGHT (same as 'l')
col = (col + 1) % 8;
assert(col == 4);
printf("PASSED\n");
}
// Test 21: Command mode parsing - quit command
void test_command_mode_quit(void) {
printf("Test 21: Command mode quit command... ");
// Test that ":q" command is recognized
const char *cmd = "q";
assert(strcmp(cmd, "q") == 0);
printf("PASSED\n");
}
// Test 22: Command mode parsing - empty command
void test_command_mode_empty(void) {
printf("Test 22: Command mode empty command... ");
// Test that empty command doesn't quit
const char *cmd = "";
assert(strcmp(cmd, "q") != 0);
printf("PASSED\n");
}
// Test 23: Command mode parsing - unknown command
void test_command_mode_unknown(void) {
printf("Test 23: Command mode unknown command... ");
// Test that unknown commands don't quit
const char *cmd = "unknown";
assert(strcmp(cmd, "q") != 0);
printf("PASSED\n");
}
// Test 24: Command mode buffer overflow protection
void test_command_mode_buffer_overflow(void) {
printf("Test 24: Command mode buffer overflow protection... ");
// Test that buffer doesn't overflow with long input
char cmd_buffer[256];
int cmd_pos = 0;
memset(cmd_buffer, 0, sizeof(cmd_buffer));
// Simulate typing more characters than buffer can hold
for (int i = 0; i < 300; i++) {
if (cmd_pos < (int)sizeof(cmd_buffer) - 1) {
cmd_buffer[cmd_pos++] = 'a';
}
}
cmd_buffer[cmd_pos] = '\0';
// Buffer should not overflow
assert(strlen(cmd_buffer) < sizeof(cmd_buffer));
assert(cmd_pos <= (int)sizeof(cmd_buffer) - 1);
printf("PASSED\n");
}
// Test 25: Command mode backspace handling
void test_command_mode_backspace(void) {
printf("Test 25: Command mode backspace handling... ");
// Test backspace removes characters
char cmd_buffer[256];
int cmd_pos = 0;
memset(cmd_buffer, 0, sizeof(cmd_buffer));
// Type "test"
cmd_buffer[cmd_pos++] = 't';
cmd_buffer[cmd_pos++] = 'e';
cmd_buffer[cmd_pos++] = 's';
cmd_buffer[cmd_pos++] = 't';
cmd_buffer[cmd_pos] = '\0';
assert(strcmp(cmd_buffer, "test") == 0);
// Backspace twice
cmd_pos--;
cmd_buffer[cmd_pos] = '\0';
cmd_pos--;
cmd_buffer[cmd_pos] = '\0';
assert(strcmp(cmd_buffer, "te") == 0);
printf("PASSED\n");
}
// Test 26: Command mode escape cancels
void test_command_mode_escape(void) {
printf("Test 26: Command mode escape cancels... ");
// Test that escape key (27) cancels command mode
int ch = 27;
assert(ch == 27); // Escape
printf("PASSED\n");
}
// Test 27: Command mode enter executes
void test_command_mode_enter(void) {
printf("Test 27: Command mode enter executes... ");
// Test that enter key executes command
int ch = '\n';
assert(ch == '\n');
ch = '\r';
assert(ch == '\r');
printf("PASSED\n");
}
// Test 28: Command mode colon triggers mode
void test_command_mode_colon(void) {
printf("Test 28: Command mode colon triggers mode... ");
// Test that ':' character triggers command mode
char ch = ':';
assert(ch == ':');
printf("PASSED\n");
}
// Test 29: Visual mode entry
void test_visual_mode_entry(void) {
printf("Test 29: Visual mode entry... ");
// Simulate pressing 'v' to enter visual mode
int current_mode = 1; // MODE_VISUAL
int selected_row = 3, selected_col = 4;
int visual_start_row = 0, visual_start_col = 0;
int visual_end_row = 0, visual_end_col = 0;
// Press 'v'
current_mode = 1; // MODE_VISUAL
visual_start_row = selected_row;
visual_start_col = selected_col;
visual_end_row = selected_row;
visual_end_col = selected_col;
assert(current_mode == 1);
assert(visual_start_row == 3);
assert(visual_start_col == 4);
assert(visual_end_row == 3);
assert(visual_end_col == 4);
printf("PASSED\n");
}
// Test 30: Visual mode selection expansion
void test_visual_mode_selection(void) {
printf("Test 30: Visual mode selection expansion... ");
int visual_start_row = 2, visual_start_col = 2;
int visual_end_row = 2, visual_end_col = 2;
// Move right
visual_end_col = (visual_end_col + 1) % 8;
assert(visual_end_col == 3);
// Move down
visual_end_row = (visual_end_row + 1) % 8;
assert(visual_end_row == 3);
// Check selection bounds
int min_row = (visual_start_row < visual_end_row) ? visual_start_row : visual_end_row;
int max_row = (visual_start_row > visual_end_row) ? visual_start_row : visual_end_row;
int min_col = (visual_start_col < visual_end_col) ? visual_start_col : visual_end_col;
int max_col = (visual_start_col > visual_end_col) ? visual_start_col : visual_end_col;
assert(min_row == 2);
assert(max_row == 3);
assert(min_col == 2);
assert(max_col == 3);
printf("PASSED\n");
}
// Test 31: Visual mode escape returns to normal
void test_visual_mode_escape(void) {
printf("Test 31: Visual mode escape returns to normal... ");
int current_mode = 1; // MODE_VISUAL
// Press Escape
current_mode = 0; // MODE_NORMAL
assert(current_mode == 0);
printf("PASSED\n");
}
// Test 32: Visual line selection
void test_visual_line_selection(void) {
printf("Test 32: Visual line selection... ");
int selected_row = 3;
int current_mode = 0; // MODE_NORMAL
int visual_start_row = 0, visual_start_col = 0;
int visual_end_row = 0, visual_end_col = 0;
// Press 'V'
current_mode = 1; // MODE_VISUAL
visual_start_row = selected_row;
visual_start_col = 0;
visual_end_row = selected_row;
visual_end_col = 7; // GRID_COLS - 1
assert(current_mode == 1);
assert(visual_start_row == 3);
assert(visual_start_col == 0);
assert(visual_end_row == 3);
assert(visual_end_col == 7);
printf("PASSED\n");
}
// Test 33: Move mode entry and navigation
void test_move_mode_navigation(void) {
printf("Test 33: Move mode navigation... ");
int current_mode = 0; // MODE_NORMAL
int selected_row = 3, selected_col = 4;
// Enter move mode
current_mode = 2; // MODE_MOVE
assert(current_mode == 2);
// Move left
selected_col = (selected_col - 1 + 8) % 8;
assert(selected_col == 3);
// Move down
selected_row = (selected_row + 1) % 8;
assert(selected_row == 4);
// Move up
selected_row = (selected_row - 1 + 8) % 8;
assert(selected_row == 3);
// Move right
selected_col = (selected_col + 1) % 8;
assert(selected_col == 4);
printf("PASSED\n");
}
// Test 34: Move mode returns to normal on enter
void test_move_mode_enter(void) {
printf("Test 34: Move mode returns to normal on enter... ");
int current_mode = 2; // MODE_MOVE
// Press Enter
current_mode = 0; // MODE_NORMAL
assert(current_mode == 0);
printf("PASSED\n");
}
// Test 35: Move mode returns to normal on escape
void test_move_mode_escape(void) {
printf("Test 35: Move mode returns to normal on escape... ");
int current_mode = 2; // MODE_MOVE
// Press Escape
current_mode = 0; // MODE_NORMAL
assert(current_mode == 0);
printf("PASSED\n");
}
// Test 36: Delete (reset) single clip
void test_delete_single_clip(void) {
printf("Test 36: Delete (reset) single clip... ");
AppState *state = create_test_state();
// Set up a looping clip
int clip_idx = 10;
state->clips[clip_idx].state = CLIP_LOOPING;
state->clips[clip_idx].buffer_size = 100;
state->clips[clip_idx].write_position = 100;
state->clips[clip_idx].read_position = 50;
// Simulate pressing 'd' on selected clip
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clip_idx };
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_EMPTY);
assert(state->clips[clip_idx].buffer_size == 0);
assert(state->clips[clip_idx].write_position == 0);
assert(state->clips[clip_idx].read_position == 0);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 37: Delete (reset) multiple clips via visual selection
void test_delete_visual_selection(void) {
printf("Test 37: Delete visual selection... ");
AppState *state = create_test_state();
// Set up clips in a 2x2 selection area
int clips[] = {18, 19, 26, 27}; // rows 2-3, cols 2-3
for (int i = 0; i < 4; i++) {
state->clips[clips[i]].state = CLIP_LOOPING;
state->clips[clips[i]].buffer_size = 100;
state->clips[clips[i]].write_position = 100;
state->clips[clips[i]].read_position = 50;
}
// Simulate deleting the selection
for (int i = 0; i < 4; i++) {
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clips[i] };
reducer(state, action);
}
for (int i = 0; i < 4; i++) {
assert(state->clips[clips[i]].state == CLIP_EMPTY);
assert(state->clips[clips[i]].buffer_size == 0);
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 38: Yank single clip
void test_yank_single_clip(void) {
printf("Test 38: Yank single clip... ");
AppState *state = create_test_state();
// Set up a clip
int clip_idx = 15;
state->clips[clip_idx].state = CLIP_LOOPING;
state->clips[clip_idx].buffer_size = 100;
// Simulate yanking the clip - should stop it (looping -> stopped)
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx };
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_STOPPED);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 39: Yank multiple clips via visual selection
void test_yank_visual_selection(void) {
printf("Test 39: Yank visual selection... ");
AppState *state = create_test_state();
// Set up clips in a 2x2 selection
int clips[] = {18, 19, 26, 27};
for (int i = 0; i < 4; i++) {
state->clips[clips[i]].state = CLIP_LOOPING;
state->clips[clips[i]].buffer_size = 100;
}
// Simulate yanking the selection - should stop all clips
for (int i = 0; i < 4; i++) {
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clips[i] };
reducer(state, action);
}
for (int i = 0; i < 4; i++) {
assert(state->clips[clips[i]].state == CLIP_STOPPED);
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 40: Paste clips
void test_paste_clips(void) {
printf("Test 40: Paste clips... ");
AppState *state = create_test_state();
// Simulate yanking clip at position (1, 1) = clip 9
int yank_buffer[] = {9};
int selected_row = 3, selected_col = 3; // Paste at position (3, 3) = clip 27
// Calculate offset
int first_yanked_row = yank_buffer[0] / 8;
int first_yanked_col = yank_buffer[0] % 8;
int row_offset = selected_row - first_yanked_row;
int col_offset = selected_col - first_yanked_col;
assert(first_yanked_row == 1);
assert(first_yanked_col == 1);
assert(row_offset == 2);
assert(col_offset == 2);
// Simulate paste: trigger clip three times to go empty -> recording -> looping -> stopped
int new_row = first_yanked_row + row_offset;
int new_col = first_yanked_col + col_offset;
int new_clip_idx = new_row * 8 + new_col;
assert(new_row == 3);
assert(new_col == 3);
assert(new_clip_idx == 27);
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = new_clip_idx };
reducer(state, action);
reducer(state, action);
reducer(state, action);
assert(state->clips[27].state == CLIP_STOPPED);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 41: Paste with bounds checking
void test_paste_bounds_checking(void) {
printf("Test 41: Paste bounds checking... ");
AppState *state = create_test_state();
// Yank clip at position (7, 7) = clip 63 (bottom-right)
int yank_buffer[] = {63};
int selected_row = 0, selected_col = 0; // Paste at top-left
// Calculate offset
int first_yanked_row = yank_buffer[0] / 8;
int first_yanked_col = yank_buffer[0] % 8;
int row_offset = selected_row - first_yanked_row;
int col_offset = selected_col - first_yanked_col;
assert(row_offset == -7);
assert(col_offset == -7);
// Simulate paste: new position should be (0, 0) = clip 0
int new_row = first_yanked_row + row_offset;
int new_col = first_yanked_col + col_offset;
assert(new_row == 0);
assert(new_col == 0);
// Test out-of-bounds paste (should be clipped)
selected_row = 0;
selected_col = 0;
row_offset = selected_row - 0;
col_offset = selected_col - 0;
// Yank clip at (0, 0) and try to paste at (0, 0) - should work
new_row = 0 + row_offset;
new_col = 0 + col_offset;
assert(new_row >= 0 && new_row < 8 && new_col >= 0 && new_col < 8);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 42: Mark setting
void test_mark_setting(void) {
printf("Test 42: Mark setting... ");
int marks[26];
for (int i = 0; i < 26; i++) {
marks[i] = -1;
}
int selected_row = 3, selected_col = 4;
int clip_idx = selected_row * 8 + selected_col;
// Set mark 'a'
char mark_char = 'a';
int idx = mark_char - 'a';
marks[idx] = clip_idx;
assert(marks[0] == 28); // 3 * 8 + 4 = 28
// Set mark 'z'
mark_char = 'z';
idx = mark_char - 'a';
marks[idx] = clip_idx;
assert(marks[25] == 28);
printf("PASSED\n");
}
// Test 43: Go to mark
void test_go_to_mark(void) {
printf("Test 43: Go to mark... ");
int marks[26];
for (int i = 0; i < 26; i++) {
marks[i] = -1;
}
// Set mark 'b' to clip 42
marks[1] = 42; // 'b' - 'a' = 1
// Go to mark 'b'
char mark_char = 'b';
int idx = mark_char - 'a';
int clip_idx = marks[idx];
assert(clip_idx == 42);
// Calculate row and col
int row = clip_idx / 8;
int col = clip_idx % 8;
assert(row == 5);
assert(col == 2);
printf("PASSED\n");
}
// Test 44: Go to unset mark
void test_go_to_unset_mark(void) {
printf("Test 44: Go to unset mark... ");
int marks[26];
for (int i = 0; i < 26; i++) {
marks[i] = -1;
}
// Try to go to unset mark 'c'
char mark_char = 'c';
int idx = mark_char - 'a';
int clip_idx = marks[idx];
assert(clip_idx == -1); // Mark not set
printf("PASSED\n");
}
// Test 45: Play next scene
void test_play_next_scene(void) {
printf("Test 45: Play next scene... ");
AppState *state = create_test_state();
int selected_row = 3;
// Play next scene
int next_row = (selected_row + 1) % 8;
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = next_row };
reducer(state, action);
assert(next_row == 4);
// Verify clips in scene 4 are recording
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(4, ch);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 46: Play previous scene
void test_play_prev_scene(void) {
printf("Test 46: Play previous scene... ");
AppState *state = create_test_state();
int selected_row = 3;
// Play previous scene
int prev_row = (selected_row - 1 + 8) % 8;
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = prev_row };
reducer(state, action);
assert(prev_row == 2);
// Verify clips in scene 2 are recording
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(2, ch);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 47: Play next scene wraps around
void test_play_next_scene_wrap(void) {
printf("Test 47: Play next scene wraps around... ");
AppState *state = create_test_state();
int selected_row = 7; // Last row
// Play next scene should wrap to row 0
int next_row = (selected_row + 1) % 8;
assert(next_row == 0);
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = next_row };
reducer(state, action);
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(0, ch);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 48: Play previous scene wraps around
void test_play_prev_scene_wrap(void) {
printf("Test 48: Play previous scene wraps around... ");
AppState *state = create_test_state();
int selected_row = 0; // First row
// Play previous scene should wrap to row 7
int prev_row = (selected_row - 1 + 8) % 8;
assert(prev_row == 7);
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = prev_row };
reducer(state, action);
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(7, ch);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
}
destroy_test_state(state);
printf("PASSED\n");
}
// Test 49: Visual mode delete then escape
void test_visual_delete_then_escape(void) {
printf("Test 49: Visual mode delete then escape... ");
AppState *state = create_test_state();
// Set up clips in visual selection
int clips[] = {18, 19, 26, 27};
for (int i = 0; i < 4; i++) {
state->clips[clips[i]].state = CLIP_LOOPING;
state->clips[clips[i]].buffer_size = 100;
}
// Simulate visual mode delete
for (int i = 0; i < 4; i++) {
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clips[i] };
reducer(state, action);
}
// Verify clips are reset
for (int i = 0; i < 4; i++) {
assert(state->clips[clips[i]].state == CLIP_EMPTY);
}
// Simulate returning to normal mode
int current_mode = 0; // MODE_NORMAL
assert(current_mode == 0);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 50: Visual mode yank then escape
void test_visual_yank_then_escape(void) {
printf("Test 50: Visual mode yank then escape... ");
AppState *state = create_test_state();
// Set up clips in visual selection
int clips[] = {18, 19, 26, 27};
for (int i = 0; i < 4; i++) {
state->clips[clips[i]].state = CLIP_LOOPING;
state->clips[clips[i]].buffer_size = 100;
}
// Simulate yanking the selection - should stop all clips
for (int i = 0; i < 4; i++) {
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clips[i] };
reducer(state, action);
}
// Verify clips are stopped
for (int i = 0; i < 4; i++) {
assert(state->clips[clips[i]].state == CLIP_STOPPED);
}
// Simulate returning to normal mode
int current_mode = 0; // MODE_NORMAL
assert(current_mode == 0);
destroy_test_state(state);
printf("PASSED\n");
}
// Test 51: Multiple marks
void test_multiple_marks(void) {
printf("Test 51: Multiple marks... ");
int marks[26];
for (int i = 0; i < 26; i++) {
marks[i] = -1;
}
// Set multiple marks
marks[0] = 0; // 'a' = clip 0
marks[1] = 63; // 'b' = clip 63
marks[2] = 28; // 'c' = clip 28
assert(marks[0] == 0);
assert(marks[1] == 63);
assert(marks[2] == 28);
// Go to mark 'b'
int clip_idx = marks[1];
int row = clip_idx / 8;
int col = clip_idx % 8;
assert(row == 7);
assert(col == 7);
printf("PASSED\n");
}
// Test 52: Re-mark existing mark
void test_remark_existing_mark(void) {
printf("Test 52: Re-mark existing mark... ");
int marks[26];
for (int i = 0; i < 26; i++) {
marks[i] = -1;
}
// Set mark 'a' to clip 10
marks[0] = 10;
assert(marks[0] == 10);
// Re-mark 'a' to clip 42
marks[0] = 42;
assert(marks[0] == 42);
printf("PASSED\n");
}
// Test 53: Undo single clip trigger
void test_undo_single_trigger(void) {
printf("Test 53: Undo single clip trigger... ");
AppState *state = create_test_state();
// Start with empty clip
int clip_idx = 10;
assert(state->clips[clip_idx].state == CLIP_EMPTY);
// Trigger clip (empty -> recording)
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx };
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
// Undo: should go back to empty
action.type = ACTION_UNDO;
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_EMPTY);
printf("PASSED\n");
destroy_test_state(state);
}
// Test 54: Undo multiple clip triggers
void test_undo_multiple_triggers(void) {
printf("Test 54: Undo multiple clip triggers... ");
AppState *state = create_test_state();
int clip1 = 10, clip2 = 11, clip3 = 12;
// Trigger three clips
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip1 };
reducer(state, action);
assert(state->clips[clip1].state == CLIP_RECORDING);
action.data.trigger_clip.clip_index = clip2;
reducer(state, action);
assert(state->clips[clip2].state == CLIP_RECORDING);
action.data.trigger_clip.clip_index = clip3;
reducer(state, action);
assert(state->clips[clip3].state == CLIP_RECORDING);
// Undo last action: clip3 should go back to empty
action.type = ACTION_UNDO;
reducer(state, action);
assert(state->clips[clip3].state == CLIP_EMPTY);
assert(state->clips[clip2].state == CLIP_RECORDING);
assert(state->clips[clip1].state == CLIP_RECORDING);
// Undo again: clip2 should go back to empty
reducer(state, action);
assert(state->clips[clip2].state == CLIP_EMPTY);
assert(state->clips[clip1].state == CLIP_RECORDING);
printf("PASSED\n");
destroy_test_state(state);
}
// Test 55: Redo single undo
void test_redo_single_undo(void) {
printf("Test 55: Redo single undo... ");
AppState *state = create_test_state();
int clip_idx = 10;
// Trigger clip
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx };
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
// Undo
action.type = ACTION_UNDO;
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_EMPTY);
// Redo: should go back to recording
action.type = ACTION_REDO;
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
printf("PASSED\n");
destroy_test_state(state);
}
// Test 56: Redo after multiple undos
void test_redo_multiple_undos(void) {
printf("Test 56: Redo after multiple undos... ");
AppState *state = create_test_state();
int clip1 = 10, clip2 = 11;
// Trigger two clips
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip1 };
reducer(state, action);
action.data.trigger_clip.clip_index = clip2;
reducer(state, action);
assert(state->clips[clip1].state == CLIP_RECORDING);
assert(state->clips[clip2].state == CLIP_RECORDING);
// Undo twice
action.type = ACTION_UNDO;
reducer(state, action);
reducer(state, action);
assert(state->clips[clip1].state == CLIP_EMPTY);
assert(state->clips[clip2].state == CLIP_EMPTY);
// Redo twice
action.type = ACTION_REDO;
reducer(state, action);
assert(state->clips[clip1].state == CLIP_RECORDING);
assert(state->clips[clip2].state == CLIP_EMPTY);
reducer(state, action);
assert(state->clips[clip1].state == CLIP_RECORDING);
assert(state->clips[clip2].state == CLIP_RECORDING);
printf("PASSED\n");
destroy_test_state(state);
}
// Test 57: Undo scene trigger
void test_undo_scene_trigger(void) {
printf("Test 57: Undo scene trigger... ");
AppState *state = create_test_state();
int scene_idx = 3;
// Trigger scene
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = scene_idx };
reducer(state, action);
// All clips in scene should be recording
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(scene_idx, ch);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
}
// Undo: all clips should go back to empty
action.type = ACTION_UNDO;
reducer(state, action);
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(scene_idx, ch);
assert(state->clips[clip_idx].state == CLIP_EMPTY);
}
printf("PASSED\n");
destroy_test_state(state);
}
// Test 58: Undo clip state cycle (empty -> recording -> looping -> stopped)
void test_undo_clip_state_cycle(void) {
printf("Test 58: Undo clip state cycle... ");
AppState *state = create_test_state();
int clip_idx = 10;
// Cycle through all states
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx };
reducer(state, action); // empty -> recording
assert(state->clips[clip_idx].state == CLIP_RECORDING);
reducer(state, action); // recording -> looping
assert(state->clips[clip_idx].state == CLIP_LOOPING);
reducer(state, action); // looping -> stopped
assert(state->clips[clip_idx].state == CLIP_STOPPED);
// Undo three times to go back to empty
action.type = ACTION_UNDO;
reducer(state, action); // stopped -> looping
assert(state->clips[clip_idx].state == CLIP_LOOPING);
reducer(state, action); // looping -> recording
assert(state->clips[clip_idx].state == CLIP_RECORDING);
reducer(state, action); // recording -> empty
assert(state->clips[clip_idx].state == CLIP_EMPTY);
printf("PASSED\n");
destroy_test_state(state);
}
// Test 59: Undo after new action clears redo history
void test_undo_clears_redo_on_new_action(void) {
printf("Test 59: Undo after new action clears redo history... ");
AppState *state = create_test_state();
int clip1 = 10, clip2 = 11;
// Trigger clip1
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip1 };
reducer(state, action);
assert(state->clips[clip1].state == CLIP_RECORDING);
// Undo
action.type = ACTION_UNDO;
reducer(state, action);
assert(state->clips[clip1].state == CLIP_EMPTY);
// Redo should work
action.type = ACTION_REDO;
reducer(state, action);
assert(state->clips[clip1].state == CLIP_RECORDING);
// Undo again
action.type = ACTION_UNDO;
reducer(state, action);
assert(state->clips[clip1].state == CLIP_EMPTY);
// Now do a new action (trigger clip2)
action.type = ACTION_TRIGGER_CLIP;
action.data.trigger_clip.clip_index = clip2;
reducer(state, action);
assert(state->clips[clip2].state == CLIP_RECORDING);
// Redo should NOT work now (redo history cleared)
action.type = ACTION_REDO;
reducer(state, action);
assert(state->clips[clip1].state == CLIP_EMPTY); // Should still be empty
printf("PASSED\n");
destroy_test_state(state);
}
// Test 60: Undo reset clip
void test_undo_reset_clip(void) {
printf("Test 60: Undo reset clip... ");
AppState *state = create_test_state();
int clip_idx = 10;
// Set up a looping clip with data
state->clips[clip_idx].state = CLIP_LOOPING;
state->clips[clip_idx].buffer_size = 100;
state->clips[clip_idx].write_position = 100;
state->clips[clip_idx].read_position = 50;
// Reset the clip
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clip_idx };
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_EMPTY);
assert(state->clips[clip_idx].buffer_size == 0);
// Undo: should restore clip to previous state
action.type = ACTION_UNDO;
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_LOOPING);
assert(state->clips[clip_idx].buffer_size == 100);
assert(state->clips[clip_idx].write_position == 100);
assert(state->clips[clip_idx].read_position == 50);
printf("PASSED\n");
destroy_test_state(state);
}
// Test 64: Undo/redo with paste operation
void test_undo_paste(void) {
printf("Test 64: Undo paste operation... ");
AppState *state = create_test_state();
int clip_idx = 27;
// Simulate paste: trigger clip three times to go empty -> recording -> looping -> stopped
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx };
reducer(state, action);
reducer(state, action);
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_STOPPED);
// Undo: should go back to looping (undo last trigger: stopped -> looping)
action.type = ACTION_UNDO;
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_LOOPING);
// Undo again: should go back to recording
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
// Undo again: should go back to empty
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_EMPTY);
// Redo three times: should go back to stopped
action.type = ACTION_REDO;
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_RECORDING);
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_LOOPING);
reducer(state, action);
assert(state->clips[clip_idx].state == CLIP_STOPPED);
printf("PASSED\n");
destroy_test_state(state);
}
int main(void) {
printf("Running TUI tests...\n\n");
test_grid_to_clip_index();
test_trigger_via_grid();
test_reset_via_grid();
test_scene_via_grid();
test_quantize_cycling();
test_threshold_toggle();
test_transport_reset_via_tui();
test_navigation_wrapping();
test_multiple_clip_states();
test_buffer_size_display();
test_help_toggle();
test_escape_handling();
test_tui_init_cleanup();
test_state_to_color_mapping();
test_full_grid_coverage();
test_scene_from_each_row();
test_quantize_full_cycle();
test_multiple_threshold_toggles();
test_multiple_transport_resets();
test_arrow_key_navigation();
test_command_mode_quit();
test_command_mode_empty();
test_command_mode_unknown();
test_command_mode_buffer_overflow();
test_command_mode_backspace();
test_command_mode_escape();
test_command_mode_enter();
test_command_mode_colon();
test_visual_mode_entry();
test_visual_mode_selection();
test_visual_mode_escape();
test_visual_line_selection();
test_move_mode_navigation();
test_move_mode_enter();
test_move_mode_escape();
test_delete_single_clip();
test_delete_visual_selection();
test_yank_single_clip();
test_yank_visual_selection();
test_paste_clips();
test_paste_bounds_checking();
test_mark_setting();
test_go_to_mark();
test_go_to_unset_mark();
test_play_next_scene();
test_play_prev_scene();
test_play_next_scene_wrap();
test_play_prev_scene_wrap();
test_visual_delete_then_escape();
test_visual_yank_then_escape();
test_multiple_marks();
test_remark_existing_mark();
test_undo_single_trigger();
test_undo_multiple_triggers();
test_redo_single_undo();
test_redo_multiple_undos();
test_undo_scene_trigger();
test_undo_clip_state_cycle();
test_undo_clears_redo_on_new_action();
test_undo_reset_clip();
test_undo_paste();
printf("\nAll TUI tests passed!\n");
return 0;
}