1575 lines
46 KiB
C
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;
|
|
}
|