#include #include #include #include #include #include "dispatcher.h" #include "transport.h" #include "engine.h" // ============================================================ // Test helpers // ============================================================ static AppState* create_test_state(void) { AppState *state = (AppState *)calloc(1, sizeof(AppState)); assert(state != NULL); state->transport_state = TRANSPORT_STOPPED; state->clock_source = CLOCK_SOURCE_INTERNAL; state->bpm = DEFAULT_BPM; state->sample_rate = 48000; state->samples_per_beat = (state->sample_rate * 60.0) / state->bpm; state->sample_accumulator = 0.0; state->quantize_mode = QUANTIZE_OFF; state->quantize_threshold = 0; state->running = true; 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 midi clips state->midi_clips[i].state = CLIP_EMPTY; state->midi_clips[i].events = (MidiEvent *)calloc(MAX_MIDI_EVENTS, sizeof(MidiEvent)); assert(state->midi_clips[i].events != NULL); state->midi_clips[i].max_events = MAX_MIDI_EVENTS; state->midi_clips[i].event_count = 0; state->midi_clips[i].read_index = 0; } 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->midi_clips[i].events); state->midi_clips[i].events = NULL; } free(state); } } // ============================================================ // Test 1: Initial state is empty // ============================================================ void test_initial_state(void) { printf("Test 1: Initial state is empty... "); AppState *state = create_test_state(); for (int i = 0; i < MAX_CLIPS; i++) { assert(state->clips[i].state == CLIP_EMPTY); assert(state->clips[i].buffer_size == 0); assert(state->clips[i].write_position == 0); assert(state->clips[i].read_position == 0); } destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 2: Trigger empty clip starts recording // ============================================================ void test_trigger_empty_starts_recording(void) { printf("Test 2: Trigger empty clip starts recording... "); AppState *state = create_test_state(); Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; reducer(state, action); assert(state->clips[0].state == CLIP_RECORDING); assert(state->clips[0].write_position == 0); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 3: Trigger recording clip stops and starts looping // ============================================================ void test_trigger_recording_starts_looping(void) { printf("Test 3: Trigger recording clip stops and starts looping... "); AppState *state = create_test_state(); // Start recording Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; reducer(state, action); assert(state->clips[0].state == CLIP_RECORDING); // Simulate some recording state->clips[0].write_position = 100; // Write data into the ring buffer so the reducer can copy it for (size_t i = 0; i < 100; i++) { state->record_buffer[0][i] = (float)i; } atomic_store(&state->record_write_pos[0], 100); // Trigger again to stop recording and start looping reducer(state, action); assert(state->clips[0].state == CLIP_LOOPING); assert(state->clips[0].buffer_size == 100); assert(state->clips[0].read_position == 0); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 4: Trigger looping clip stops it // ============================================================ void test_trigger_looping_stops(void) { printf("Test 4: Trigger looping clip stops it... "); AppState *state = create_test_state(); // Set up a looping clip state->clips[0].state = CLIP_LOOPING; state->clips[0].buffer_size = 100; state->clips[0].read_position = 50; Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; reducer(state, action); assert(state->clips[0].state == CLIP_STOPPED); assert(state->clips[0].read_position == 0); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 5: Trigger stopped clip starts looping again // ============================================================ void test_trigger_stopped_resumes_looping(void) { printf("Test 5: Trigger stopped clip starts looping again... "); AppState *state = create_test_state(); // Set up a stopped clip state->clips[0].state = CLIP_STOPPED; state->clips[0].buffer_size = 100; Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; reducer(state, action); assert(state->clips[0].state == CLIP_LOOPING); assert(state->clips[0].read_position == 0); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 6: Full cycle test // ============================================================ void test_full_cycle(void) { printf("Test 6: Full cycle test... "); AppState *state = create_test_state(); Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; // Empty -> Recording reducer(state, action); assert(state->clips[0].state == CLIP_RECORDING); // Recording -> Looping state->clips[0].write_position = 200; // Write data into the ring buffer so the reducer can copy it for (size_t i = 0; i < 200; i++) { state->record_buffer[0][i] = (float)i; } atomic_store(&state->record_write_pos[0], 200); reducer(state, action); assert(state->clips[0].state == CLIP_LOOPING); assert(state->clips[0].buffer_size == 200); // Looping -> Stopped reducer(state, action); assert(state->clips[0].state == CLIP_STOPPED); // Stopped -> Looping reducer(state, action); assert(state->clips[0].state == CLIP_LOOPING); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 7: Multiple clips work independently // ============================================================ void test_multiple_clips(void) { printf("Test 7: Multiple clips work independently... "); AppState *state = create_test_state(); Action action0 = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; Action action1 = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 1 } }; Action action2 = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 2 } }; // Clip 0: Empty -> Recording reducer(state, action0); assert(state->clips[0].state == CLIP_RECORDING); // Clip 1: Empty -> Recording reducer(state, action1); assert(state->clips[1].state == CLIP_RECORDING); // Clip 0: Recording -> Looping state->clips[0].write_position = 100; reducer(state, action0); assert(state->clips[0].state == CLIP_LOOPING); assert(state->clips[1].state == CLIP_RECORDING); // Clip 1 unaffected // Clip 2: Empty -> Recording reducer(state, action2); assert(state->clips[2].state == CLIP_RECORDING); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 8: Reset clip // ============================================================ void test_reset_clip(void) { printf("Test 8: Reset clip... "); AppState *state = create_test_state(); // Set up a clip with data state->clips[0].state = CLIP_LOOPING; state->clips[0].buffer_size = 100; state->clips[0].write_position = 100; state->clips[0].read_position = 50; Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = 0 } }; reducer(state, action); assert(state->clips[0].state == CLIP_EMPTY); assert(state->clips[0].buffer_size == 0); assert(state->clips[0].write_position == 0); assert(state->clips[0].read_position == 0); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 9: Clip state to velocity mapping // ============================================================ void test_state_to_velocity(void) { printf("Test 9: Clip state to velocity mapping... "); assert(clip_state_to_velocity(CLIP_EMPTY) == 0); assert(clip_state_to_velocity(CLIP_RECORDING) == 64); assert(clip_state_to_velocity(CLIP_LOOPING) == 127); assert(clip_state_to_velocity(CLIP_STOPPED) == 32); printf("PASSED\n"); } // ============================================================ // Test 10: Clip state to string // ============================================================ void test_state_to_string(void) { printf("Test 10: Clip state to string... "); assert(strcmp(clip_state_to_string(CLIP_EMPTY), "Empty") == 0); assert(strcmp(clip_state_to_string(CLIP_RECORDING), "Recording") == 0); assert(strcmp(clip_state_to_string(CLIP_LOOPING), "Looping") == 0); assert(strcmp(clip_state_to_string(CLIP_STOPPED), "Stopped") == 0); printf("PASSED\n"); } // ============================================================ // Test 11: Invalid clip index // ============================================================ void test_invalid_clip_index(void) { printf("Test 11: Invalid clip index... "); AppState *state = create_test_state(); // These should not crash Action action_neg = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = -1 } }; Action action_max = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = MAX_CLIPS } }; Action reset_neg = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = -1 } }; Action reset_max = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = MAX_CLIPS } }; reducer(state, action_neg); reducer(state, action_max); reducer(state, reset_neg); reducer(state, reset_max); // Verify no state changes assert(state->clips[0].state == CLIP_EMPTY); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 12: Buffer overflow protection // ============================================================ void test_buffer_overflow(void) { printf("Test 12: Buffer overflow protection... "); AppState *state = create_test_state(); Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; // Start recording reducer(state, action); assert(state->clips[0].state == CLIP_RECORDING); // Fill buffer to max state->clips[0].write_position = MAX_BUFFER_SIZE; // Write data into the ring buffer so the reducer can copy it for (size_t i = 0; i < MAX_BUFFER_SIZE; i++) { state->record_buffer[0][i] = (float)i; } atomic_store(&state->record_write_pos[0], MAX_BUFFER_SIZE); // Trigger should stop recording and start looping reducer(state, action); assert(state->clips[0].state == CLIP_LOOPING); assert(state->clips[0].buffer_size == MAX_BUFFER_SIZE); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 13: Transport initial state // ============================================================ void test_transport_initial_state(void) { printf("Test 13: Transport initial state... "); AppState *state = create_test_state(); 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 14: Transport reset // ============================================================ void test_transport_reset(void) { printf("Test 14: Transport reset... "); AppState *state = create_test_state(); // Simulate some transport state state->transport_state = TRANSPORT_PLAYING; state->clock_count = 100; state->beat_position = 2; state->bar_position = 5; state->sample_position = 10000; Action action = { .type = ACTION_RESET_TRANSPORT }; 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 15: Quantize mode setting // ============================================================ void test_quantize_mode_setting(void) { printf("Test 15: Quantize mode setting... "); AppState *state = create_test_state(); assert(state->quantize_mode == QUANTIZE_OFF); Action action_beat = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BEAT } }; Action action_bar = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BAR } }; Action action_off = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_OFF } }; reducer(state, action_beat); assert(state->quantize_mode == QUANTIZE_BEAT); reducer(state, action_bar); assert(state->quantize_mode == QUANTIZE_BAR); reducer(state, action_off); assert(state->quantize_mode == QUANTIZE_OFF); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 16: Quantize mode to string // ============================================================ void test_quantize_mode_to_string(void) { printf("Test 16: Quantize mode to string... "); assert(strcmp(quantize_mode_to_string(QUANTIZE_OFF), "Off") == 0); assert(strcmp(quantize_mode_to_string(QUANTIZE_BEAT), "Beat") == 0); assert(strcmp(quantize_mode_to_string(QUANTIZE_BAR), "Bar") == 0); printf("PASSED\n"); } // ============================================================ // Test 17: Quantize threshold setting // ============================================================ void test_quantize_threshold_setting(void) { printf("Test 17: Quantize threshold setting... "); AppState *state = create_test_state(); assert(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); action.data.set_quantize_threshold.threshold = 0; reducer(state, action); assert(state->quantize_threshold == 0); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 18: Undo/Redo basic // ============================================================ void test_undo_redo(void) { printf("Test 18: Undo/Redo basic... "); AppState *state = create_test_state(); // Trigger clip 0 (empty -> recording) Action trigger = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } }; reducer(state, trigger); assert(state->clips[0].state == CLIP_RECORDING); // Undo Action undo = { .type = ACTION_UNDO }; reducer(state, undo); assert(state->clips[0].state == CLIP_EMPTY); // Redo Action redo = { .type = ACTION_REDO }; reducer(state, redo); assert(state->clips[0].state == CLIP_RECORDING); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 19: MIDI note on triggers clip // ============================================================ void test_midi_note_on(void) { printf("Test 19: MIDI note on triggers clip... "); AppState *state = create_test_state(); Action action = { .type = ACTION_MIDI_NOTE_ON, .data.midi_note_on = { .note = 60, .velocity = 100, .channel = 0, .time = 0 } }; reducer(state, action); int clip_idx = 60 % MAX_CLIPS; assert(state->clips[clip_idx].state == CLIP_RECORDING); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 20: Scene trigger // ============================================================ void test_scene_trigger(void) { printf("Test 20: Scene trigger... "); AppState *state = create_test_state(); Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = 0 } }; 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 21: Transport play/pause/stop // ============================================================ void test_transport_play_pause_stop(void) { printf("Test 21: Transport play/pause/stop... "); AppState *state = create_test_state(); Action play = { .type = ACTION_TRANSPORT_PLAY }; Action pause = { .type = ACTION_TRANSPORT_PAUSE }; Action stop = { .type = ACTION_TRANSPORT_STOP }; Action toggle = { .type = ACTION_TRANSPORT_TOGGLE_PLAY }; reducer(state, play); assert(state->transport_state == TRANSPORT_PLAYING); reducer(state, pause); assert(state->transport_state == TRANSPORT_PAUSED); reducer(state, toggle); assert(state->transport_state == TRANSPORT_PLAYING); reducer(state, stop); 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 22: BPM setting // ============================================================ void test_bpm_setting(void) { printf("Test 22: BPM setting... "); AppState *state = create_test_state(); assert(state->bpm == DEFAULT_BPM); Action action = { .type = ACTION_SET_BPM, .data.set_bpm = { .bpm = 140.0 } }; reducer(state, action); assert(state->bpm == 140.0); double expected_spp = (state->sample_rate * 60.0) / 140.0; assert(state->samples_per_beat == expected_spp); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 23: Clock source setting // ============================================================ void test_clock_source_setting(void) { printf("Test 23: Clock source setting... "); AppState *state = create_test_state(); assert(state->clock_source == CLOCK_SOURCE_INTERNAL); Action action = { .type = ACTION_SET_CLOCK_SOURCE, .data.set_clock_source = { .source = CLOCK_SOURCE_MIDI } }; reducer(state, action); assert(state->clock_source == CLOCK_SOURCE_MIDI); 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 24: Quantization with transport rolling (simulated) // ============================================================ void test_quantization_with_transport(void) { printf("Test 24: Quantization with transport rolling... "); AppState *state = create_test_state(); state->sample_rate = 48000; state->transport_state = TRANSPORT_PLAYING; state->clock_count = MIDI_CLOCKS_PER_BEAT * 2; // 2 beats in state->sample_position = state->sample_rate * 2; // 2 beats in samples // Set quantize to beat Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BEAT } }; reducer(state, action); // Calculate next beat boundary jack_nframes_t frames_per_beat = state->sample_rate; // 48000 at 120 BPM jack_nframes_t current_pos = state->sample_position; jack_nframes_t next_beat = ((current_pos / frames_per_beat) + 1) * frames_per_beat; jack_nframes_t quantize_frame = next_beat - state->sample_position; // Should be 48000 samples to next beat assert(quantize_frame == frames_per_beat); // Test bar quantization action.data.set_quantize_mode.mode = QUANTIZE_BAR; reducer(state, action); jack_nframes_t frames_per_bar = frames_per_beat * BEATS_PER_BAR; jack_nframes_t next_bar = ((current_pos / frames_per_bar) + 1) * frames_per_bar; quantize_frame = next_bar - state->sample_position; // Should be 96000 samples to next bar (2 beats into 4-beat bar) assert(quantize_frame == frames_per_beat * 2); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 25: Quantization off with transport rolling // ============================================================ void test_quantization_off_with_transport(void) { printf("Test 25: Quantization off with transport rolling... "); AppState *state = create_test_state(); state->sample_rate = 48000; state->transport_state = TRANSPORT_PLAYING; state->clock_count = MIDI_CLOCKS_PER_BEAT * 2; state->sample_position = state->sample_rate * 2; Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_OFF } }; reducer(state, action); // When quantize is off, trigger should be immediate jack_nframes_t current_frame = 100; jack_nframes_t quantize_frame = current_frame; // Should be same as current assert(quantize_frame == 100); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 26: Quantization without transport rolling // ============================================================ void test_quantization_without_transport(void) { printf("Test 26: Quantization without transport rolling... "); AppState *state = create_test_state(); state->sample_rate = 48000; state->transport_state = TRANSPORT_STOPPED; Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = QUANTIZE_BEAT } }; reducer(state, action); // When transport is not rolling, trigger should be immediate jack_nframes_t current_frame = 100; jack_nframes_t quantize_frame = current_frame; // Should be same as current assert(quantize_frame == 100); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 27: Save/Load clip (basic) // ============================================================ void test_save_load_clip(void) { printf("Test 27: Save/Load clip (basic)... "); AppState *state = create_test_state(); // Set up a clip with data state->clips[0].state = CLIP_LOOPING; state->clips[0].buffer_size = 100; state->clips[0].write_position = 100; state->clips[0].read_position = 0; for (size_t i = 0; i < 100; i++) { state->clips[0].buffer[i] = (float)i; } // Save clip Action save = { .type = ACTION_SAVE_CLIP, .data.save_clip = { .clip_index = 0 } }; reducer(state, save); // Reset clip Action reset = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = 0 } }; reducer(state, reset); assert(state->clips[0].state == CLIP_EMPTY); // Load clip (requires a filename; we'll use a dummy that will fail gracefully) Action load = { .type = ACTION_LOAD_CLIP, .data.load_clip = { .clip_index = 0, .filename = "samples/clip_0.wav" } }; reducer(state, load); // If file doesn't exist, clip remains empty (no crash) // We just verify no crash destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 28: Quit action // ============================================================ void test_quit_action(void) { printf("Test 28: Quit action... "); AppState *state = create_test_state(); assert(state->running == true); Action quit = { .type = ACTION_QUIT }; reducer(state, quit); assert(state->running == false); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 29: MIDI clip initial state // ============================================================ void test_midi_clip_initial_state(void) { printf("Test 29: MIDI clip initial state... "); AppState *state = create_test_state(); for (int i = 0; i < MAX_CLIPS; i++) { assert(state->midi_clips[i].state == CLIP_EMPTY); assert(state->midi_clips[i].event_count == 0); assert(state->midi_clips[i].read_index == 0); assert(state->midi_clips[i].events != NULL); assert(state->midi_clips[i].max_events == MAX_MIDI_EVENTS); } destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 30: MIDI clip trigger empty starts recording // ============================================================ void test_midi_clip_trigger_empty_starts_recording(void) { printf("Test 30: MIDI clip trigger empty starts recording... "); AppState *state = create_test_state(); Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } }; reducer(state, action); assert(state->midi_clips[0].state == CLIP_RECORDING); assert(state->midi_clips[0].event_count == 0); assert(state->midi_clips[0].read_index == 0); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 31: MIDI clip trigger recording starts looping // ============================================================ void test_midi_clip_trigger_recording_starts_looping(void) { printf("Test 31: MIDI clip trigger recording starts looping... "); AppState *state = create_test_state(); // Start recording Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } }; reducer(state, action); assert(state->midi_clips[0].state == CLIP_RECORDING); // Simulate some recorded events state->midi_clips[0].event_count = 10; // Trigger again to stop recording and start looping reducer(state, action); assert(state->midi_clips[0].state == CLIP_LOOPING); assert(state->midi_clips[0].read_index == 0); assert(state->midi_clips[0].event_count == 10); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 32: MIDI clip trigger looping stops it // ============================================================ void test_midi_clip_trigger_looping_stops(void) { printf("Test 32: MIDI clip trigger looping stops it... "); AppState *state = create_test_state(); // Set up a looping MIDI clip state->midi_clips[0].state = CLIP_LOOPING; state->midi_clips[0].event_count = 10; state->midi_clips[0].read_index = 5; Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } }; reducer(state, action); assert(state->midi_clips[0].state == CLIP_STOPPED); assert(state->midi_clips[0].read_index == 0); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 33: MIDI clip trigger stopped resumes looping // ============================================================ void test_midi_clip_trigger_stopped_resumes_looping(void) { printf("Test 33: MIDI clip trigger stopped resumes looping... "); AppState *state = create_test_state(); // Set up a stopped MIDI clip state->midi_clips[0].state = CLIP_STOPPED; state->midi_clips[0].event_count = 10; Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } }; reducer(state, action); assert(state->midi_clips[0].state == CLIP_LOOPING); assert(state->midi_clips[0].read_index == 0); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 34: MIDI clip full cycle // ============================================================ void test_midi_clip_full_cycle(void) { printf("Test 34: MIDI clip full cycle... "); AppState *state = create_test_state(); Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } }; // Empty -> Recording reducer(state, action); assert(state->midi_clips[0].state == CLIP_RECORDING); // Recording -> Looping state->midi_clips[0].event_count = 20; reducer(state, action); assert(state->midi_clips[0].state == CLIP_LOOPING); assert(state->midi_clips[0].event_count == 20); // Looping -> Stopped reducer(state, action); assert(state->midi_clips[0].state == CLIP_STOPPED); // Stopped -> Looping reducer(state, action); assert(state->midi_clips[0].state == CLIP_LOOPING); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 35: MIDI clip reset // ============================================================ void test_midi_clip_reset(void) { printf("Test 35: MIDI clip reset... "); AppState *state = create_test_state(); // Set up a MIDI clip with data state->midi_clips[0].state = CLIP_LOOPING; state->midi_clips[0].event_count = 20; state->midi_clips[0].read_index = 10; Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = 0 } }; reducer(state, action); assert(state->midi_clips[0].state == CLIP_EMPTY); assert(state->midi_clips[0].event_count == 0); assert(state->midi_clips[0].read_index == 0); // events pointer should still be valid assert(state->midi_clips[0].events != NULL); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 36: MIDI clip multiple clips work independently // ============================================================ void test_midi_clip_multiple_clips(void) { printf("Test 36: MIDI clip multiple clips work independently... "); AppState *state = create_test_state(); Action action0 = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } }; Action action1 = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 1 } }; Action action2 = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 2 } }; // Clip 0: Empty -> Recording reducer(state, action0); assert(state->midi_clips[0].state == CLIP_RECORDING); // Clip 1: Empty -> Recording reducer(state, action1); assert(state->midi_clips[1].state == CLIP_RECORDING); // Clip 0: Recording -> Looping state->midi_clips[0].event_count = 10; reducer(state, action0); assert(state->midi_clips[0].state == CLIP_LOOPING); assert(state->midi_clips[1].state == CLIP_RECORDING); // Clip 1 unaffected // Clip 2: Empty -> Recording reducer(state, action2); assert(state->midi_clips[2].state == CLIP_RECORDING); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 37: MIDI clip invalid index // ============================================================ void test_midi_clip_invalid_index(void) { printf("Test 37: MIDI clip invalid index... "); AppState *state = create_test_state(); // These should not crash Action action_neg = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = -1 } }; Action action_max = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = MAX_CLIPS } }; Action reset_neg = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = -1 } }; Action reset_max = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = MAX_CLIPS } }; reducer(state, action_neg); reducer(state, action_max); reducer(state, reset_neg); reducer(state, reset_max); // Verify no state changes assert(state->midi_clips[0].state == CLIP_EMPTY); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 38: MIDI clip show_midi_grid toggle // ============================================================ void test_midi_clip_show_grid_toggle(void) { printf("Test 38: MIDI clip show_midi_grid toggle... "); AppState *state = create_test_state(); assert(state->show_midi_grid == false); Action action_on = { .type = ACTION_SET_SHOW_MIDI_GRID, .data.set_show_midi_grid = { .show = true } }; reducer(state, action_on); assert(state->show_midi_grid == true); Action action_off = { .type = ACTION_SET_SHOW_MIDI_GRID, .data.set_show_midi_grid = { .show = false } }; reducer(state, action_off); assert(state->show_midi_grid == false); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 39: MIDI clip events pointer persists after reset // ============================================================ void test_midi_clip_events_persist_after_reset(void) { printf("Test 39: MIDI clip events pointer persists after reset... "); AppState *state = create_test_state(); // Get the original events pointer MidiEvent *original_events = state->midi_clips[0].events; assert(original_events != NULL); // Set up some events state->midi_clips[0].events[0].note = 60; state->midi_clips[0].events[0].velocity = 100; state->midi_clips[0].events[0].timestamp = 0; state->midi_clips[0].event_count = 1; state->midi_clips[0].state = CLIP_LOOPING; // Reset the clip Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = 0 } }; reducer(state, action); // Events pointer should still be the same (not freed) assert(state->midi_clips[0].events == original_events); assert(state->midi_clips[0].state == CLIP_EMPTY); assert(state->midi_clips[0].event_count == 0); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Test 40: MIDI clip trigger with events in buffer // ============================================================ void test_midi_clip_trigger_with_events(void) { printf("Test 40: MIDI clip trigger with events in buffer... "); AppState *state = create_test_state(); // Set up a MIDI clip with events already recorded state->midi_clips[0].events[0].note = 60; state->midi_clips[0].events[0].velocity = 100; state->midi_clips[0].events[0].timestamp = 0; state->midi_clips[0].events[1].note = 64; state->midi_clips[0].events[1].velocity = 80; state->midi_clips[0].events[1].timestamp = 100; state->midi_clips[0].event_count = 2; state->midi_clips[0].state = CLIP_STOPPED; // Trigger to resume looping Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } }; reducer(state, action); assert(state->midi_clips[0].state == CLIP_LOOPING); assert(state->midi_clips[0].event_count == 2); assert(state->midi_clips[0].read_index == 0); // Verify events are intact assert(state->midi_clips[0].events[0].note == 60); assert(state->midi_clips[0].events[1].note == 64); destroy_test_state(state); printf("PASSED\n"); } // ============================================================ // Main // ============================================================ int main(void) { printf("Running JACK Looper tests...\n\n"); test_initial_state(); test_trigger_empty_starts_recording(); test_trigger_recording_starts_looping(); test_trigger_looping_stops(); test_trigger_stopped_resumes_looping(); test_full_cycle(); test_multiple_clips(); test_reset_clip(); test_state_to_velocity(); test_state_to_string(); test_invalid_clip_index(); test_buffer_overflow(); test_transport_initial_state(); test_transport_reset(); test_quantize_mode_setting(); test_quantize_mode_to_string(); test_quantize_threshold_setting(); test_undo_redo(); test_midi_note_on(); test_scene_trigger(); test_transport_play_pause_stop(); test_bpm_setting(); test_clock_source_setting(); test_quantization_with_transport(); test_quantization_off_with_transport(); test_quantization_without_transport(); test_save_load_clip(); test_quit_action(); test_midi_clip_initial_state(); test_midi_clip_trigger_empty_starts_recording(); test_midi_clip_trigger_recording_starts_looping(); test_midi_clip_trigger_looping_stops(); test_midi_clip_trigger_stopped_resumes_looping(); test_midi_clip_full_cycle(); test_midi_clip_reset(); test_midi_clip_multiple_clips(); test_midi_clip_invalid_index(); test_midi_clip_show_grid_toggle(); test_midi_clip_events_persist_after_reset(); test_midi_clip_trigger_with_events(); printf("\nAll tests passed!\n"); exit(0); return 0; }