fix: change reducer to take pointer to AppState to avoid stack overflow

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-05 09:32:21 +00:00
parent 4167419d54
commit 8e05c2f0ab
3 changed files with 192 additions and 194 deletions

View File

@@ -83,7 +83,7 @@ void test_trigger_empty_starts_recording(void) {
AppState *state = create_test_state();
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_RECORDING);
assert(state->clips[0].write_position == 0);
@@ -101,14 +101,14 @@ void test_trigger_recording_starts_looping(void) {
// Start recording
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_RECORDING);
// Simulate some recording
state->clips[0].write_position = 100;
// Trigger again to stop recording and start looping
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_LOOPING);
assert(state->clips[0].buffer_size == 100);
assert(state->clips[0].read_position == 0);
@@ -130,7 +130,7 @@ void test_trigger_looping_stops(void) {
state->clips[0].read_position = 50;
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_STOPPED);
assert(state->clips[0].read_position == 0);
@@ -150,7 +150,7 @@ void test_trigger_stopped_resumes_looping(void) {
state->clips[0].buffer_size = 100;
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_LOOPING);
assert(state->clips[0].read_position == 0);
@@ -168,21 +168,21 @@ void test_full_cycle(void) {
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
// Empty -> Recording
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_RECORDING);
// Recording -> Looping
state->clips[0].write_position = 200;
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_LOOPING);
assert(state->clips[0].buffer_size == 200);
// Looping -> Stopped
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_STOPPED);
// Stopped -> Looping
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_LOOPING);
destroy_test_state(state);
@@ -201,21 +201,21 @@ void test_multiple_clips(void) {
Action action2 = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 2 } };
// Clip 0: Empty -> Recording
*state = reducer(*state, action0);
reducer(state, action0);
assert(state->clips[0].state == CLIP_RECORDING);
// Clip 1: Empty -> Recording
*state = reducer(*state, action1);
reducer(state, action1);
assert(state->clips[1].state == CLIP_RECORDING);
// Clip 0: Recording -> Looping
state->clips[0].write_position = 100;
*state = reducer(*state, action0);
reducer(state, action0);
assert(state->clips[0].state == CLIP_LOOPING);
assert(state->clips[1].state == CLIP_RECORDING); // Clip 1 unaffected
// Clip 2: Empty -> Recording
*state = reducer(*state, action2);
reducer(state, action2);
assert(state->clips[2].state == CLIP_RECORDING);
destroy_test_state(state);
@@ -236,7 +236,7 @@ void test_reset_clip(void) {
state->clips[0].read_position = 50;
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_EMPTY);
assert(state->clips[0].buffer_size == 0);
assert(state->clips[0].write_position == 0);
@@ -287,10 +287,10 @@ void test_invalid_clip_index(void) {
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 } };
*state = reducer(*state, action_neg);
*state = reducer(*state, action_max);
*state = reducer(*state, reset_neg);
*state = reducer(*state, reset_max);
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);
@@ -309,14 +309,14 @@ void test_buffer_overflow(void) {
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
// Start recording
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_RECORDING);
// Fill buffer to max
state->clips[0].write_position = MAX_BUFFER_SIZE;
// Trigger should stop recording and start looping
*state = reducer(*state, action);
reducer(state, action);
assert(state->clips[0].state == CLIP_LOOPING);
assert(state->clips[0].buffer_size == MAX_BUFFER_SIZE);
@@ -356,7 +356,7 @@ void test_transport_reset(void) {
state->sample_position = 10000;
Action action = { .type = ACTION_RESET_TRANSPORT };
*state = reducer(*state, action);
reducer(state, action);
assert(state->transport_state == TRANSPORT_STOPPED);
assert(state->clock_count == 0);
@@ -381,13 +381,13 @@ void test_quantize_mode_setting(void) {
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 } };
*state = reducer(*state, action_beat);
reducer(state, action_beat);
assert(state->quantize_mode == QUANTIZE_BEAT);
*state = reducer(*state, action_bar);
reducer(state, action_bar);
assert(state->quantize_mode == QUANTIZE_BAR);
*state = reducer(*state, action_off);
reducer(state, action_off);
assert(state->quantize_mode == QUANTIZE_OFF);
destroy_test_state(state);
@@ -417,11 +417,11 @@ void test_quantize_threshold_setting(void) {
assert(state->quantize_threshold == 0);
Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold = { .threshold = 1000 } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->quantize_threshold == 1000);
action.data.set_quantize_threshold.threshold = 0;
*state = reducer(*state, action);
reducer(state, action);
assert(state->quantize_threshold == 0);
destroy_test_state(state);
@@ -437,17 +437,17 @@ void test_undo_redo(void) {
// Trigger clip 0 (empty -> recording)
Action trigger = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = 0 } };
*state = reducer(*state, trigger);
reducer(state, trigger);
assert(state->clips[0].state == CLIP_RECORDING);
// Undo
Action undo = { .type = ACTION_UNDO };
*state = reducer(*state, undo);
reducer(state, undo);
assert(state->clips[0].state == CLIP_EMPTY);
// Redo
Action redo = { .type = ACTION_REDO };
*state = reducer(*state, redo);
reducer(state, redo);
assert(state->clips[0].state == CLIP_RECORDING);
destroy_test_state(state);
@@ -462,7 +462,7 @@ void test_midi_note_on(void) {
AppState *state = create_test_state();
Action action = { .type = ACTION_MIDI_NOTE_ON, .data.midi_note_on = { .note = 60, .velocity = 100, .channel = 0, .time = 0 } };
*state = reducer(*state, action);
reducer(state, action);
int clip_idx = 60 % MAX_CLIPS;
assert(state->clips[clip_idx].state == CLIP_RECORDING);
@@ -479,7 +479,7 @@ void test_scene_trigger(void) {
AppState *state = create_test_state();
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
for (int ch = 0; ch < MAX_CHANNELS; ch++) {
int clip_idx = CLIP_INDEX(0, ch);
@@ -502,16 +502,16 @@ void test_transport_play_pause_stop(void) {
Action stop = { .type = ACTION_TRANSPORT_STOP };
Action toggle = { .type = ACTION_TRANSPORT_TOGGLE_PLAY };
*state = reducer(*state, play);
reducer(state, play);
assert(state->transport_state == TRANSPORT_PLAYING);
*state = reducer(*state, pause);
reducer(state, pause);
assert(state->transport_state == TRANSPORT_PAUSED);
*state = reducer(*state, toggle);
reducer(state, toggle);
assert(state->transport_state == TRANSPORT_PLAYING);
*state = reducer(*state, stop);
reducer(state, stop);
assert(state->transport_state == TRANSPORT_STOPPED);
assert(state->clock_count == 0);
assert(state->beat_position == 0);
@@ -532,7 +532,7 @@ void test_bpm_setting(void) {
assert(state->bpm == DEFAULT_BPM);
Action action = { .type = ACTION_SET_BPM, .data.set_bpm = { .bpm = 140.0 } };
*state = reducer(*state, action);
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);
@@ -551,7 +551,7 @@ void test_clock_source_setting(void) {
assert(state->clock_source == CLOCK_SOURCE_INTERNAL);
Action action = { .type = ACTION_SET_CLOCK_SOURCE, .data.set_clock_source = { .source = CLOCK_SOURCE_MIDI } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->clock_source == CLOCK_SOURCE_MIDI);
assert(state->clock_count == 0);
assert(state->beat_position == 0);
@@ -664,16 +664,16 @@ void test_save_load_clip(void) {
// Save clip
Action save = { .type = ACTION_SAVE_CLIP, .data.save_clip = { .clip_index = 0 } };
*state = reducer(*state, save);
reducer(state, save);
// Reset clip
Action reset = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = 0 } };
*state = reducer(*state, reset);
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" } };
*state = reducer(*state, load);
reducer(state, load);
// If file doesn't exist, clip remains empty (no crash)
// We just verify no crash
@@ -690,7 +690,7 @@ void test_quit_action(void) {
assert(state->running == true);
Action quit = { .type = ACTION_QUIT };
*state = reducer(*state, quit);
reducer(state, quit);
assert(state->running == false);
destroy_test_state(state);
@@ -724,7 +724,7 @@ void test_midi_clip_trigger_empty_starts_recording(void) {
AppState *state = create_test_state();
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->midi_clips[0].state == CLIP_RECORDING);
assert(state->midi_clips[0].event_count == 0);
@@ -743,14 +743,14 @@ void test_midi_clip_trigger_recording_starts_looping(void) {
// Start recording
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
*state = reducer(*state, action);
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
*state = reducer(*state, action);
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);
@@ -772,7 +772,7 @@ void test_midi_clip_trigger_looping_stops(void) {
state->midi_clips[0].read_index = 5;
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->midi_clips[0].state == CLIP_STOPPED);
assert(state->midi_clips[0].read_index == 0);
@@ -792,7 +792,7 @@ void test_midi_clip_trigger_stopped_resumes_looping(void) {
state->midi_clips[0].event_count = 10;
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->midi_clips[0].state == CLIP_LOOPING);
assert(state->midi_clips[0].read_index == 0);
@@ -844,7 +844,7 @@ void test_midi_clip_reset(void) {
state->midi_clips[0].read_index = 10;
Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = 0 } };
*state = reducer(*state, action);
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);
@@ -867,21 +867,21 @@ void test_midi_clip_multiple_clips(void) {
Action action2 = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 2 } };
// Clip 0: Empty -> Recording
*state = reducer(*state, action0);
reducer(state, action0);
assert(state->midi_clips[0].state == CLIP_RECORDING);
// Clip 1: Empty -> Recording
*state = reducer(*state, action1);
reducer(state, action1);
assert(state->midi_clips[1].state == CLIP_RECORDING);
// Clip 0: Recording -> Looping
state->midi_clips[0].event_count = 10;
*state = reducer(*state, action0);
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
*state = reducer(*state, action2);
reducer(state, action2);
assert(state->midi_clips[2].state == CLIP_RECORDING);
destroy_test_state(state);
@@ -901,10 +901,10 @@ void test_midi_clip_invalid_index(void) {
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 } };
*state = reducer(*state, action_neg);
*state = reducer(*state, action_max);
*state = reducer(*state, reset_neg);
*state = reducer(*state, reset_max);
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);
@@ -923,11 +923,11 @@ void test_midi_clip_show_grid_toggle(void) {
assert(state->show_midi_grid == false);
Action action_on = { .type = ACTION_SET_SHOW_MIDI_GRID, .data.set_show_midi_grid = { .show = true } };
*state = reducer(*state, action_on);
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 } };
*state = reducer(*state, action_off);
reducer(state, action_off);
assert(state->show_midi_grid == false);
destroy_test_state(state);
@@ -954,7 +954,7 @@ void test_midi_clip_events_persist_after_reset(void) {
// Reset the clip
Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
// Events pointer should still be the same (not freed)
assert(state->midi_clips[0].events == original_events);
@@ -984,7 +984,7 @@ void test_midi_clip_trigger_with_events(void) {
// Trigger to resume looping
Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = 0 } };
*state = reducer(*state, action);
reducer(state, action);
assert(state->midi_clips[0].state == CLIP_LOOPING);
assert(state->midi_clips[0].event_count == 2);