diff --git a/engine.c b/engine.c index 7d2110b..fc17955 100644 --- a/engine.c +++ b/engine.c @@ -520,22 +520,29 @@ void engine_undo(Engine *engine) { case ACTION_TRIGGER_SCENE: { int scene_idx = action->index; + if (scene_idx < 0 || scene_idx >= MAX_SCENES) break; for (int ch = 0; ch < MAX_CHANNELS; ch++) { int clip_idx = CLIP_INDEX(scene_idx, ch); - engine->clips[clip_idx].state = CLIP_EMPTY; - engine->clips[clip_idx].buffer_size = 0; - engine->clips[clip_idx].write_position = 0; - engine->clips[clip_idx].read_position = 0; + if (clip_idx < 0 || clip_idx >= MAX_CLIPS) continue; + Clip *clip = &engine->clips[clip_idx]; + if (!clip->buffer) continue; + clip->state = CLIP_EMPTY; + clip->buffer_size = 0; + clip->write_position = 0; + clip->read_position = 0; } break; } case ACTION_RESET_CLIP: { int clip_idx = action->index; - engine->clips[clip_idx].state = action->previous_state; - engine->clips[clip_idx].buffer_size = action->previous_buffer_size; - engine->clips[clip_idx].write_position = action->previous_write_position; - engine->clips[clip_idx].read_position = action->previous_read_position; + if (clip_idx < 0 || clip_idx >= MAX_CLIPS) break; + Clip *clip = &engine->clips[clip_idx]; + if (!clip->buffer) break; + clip->state = action->previous_state; + clip->buffer_size = action->previous_buffer_size; + clip->write_position = action->previous_write_position; + clip->read_position = action->previous_read_position; break; } @@ -612,9 +619,12 @@ void engine_redo(Engine *engine) { case ACTION_TRIGGER_SCENE: { int scene_idx = action->index; + if (scene_idx < 0 || scene_idx >= MAX_SCENES) break; for (int ch = 0; ch < MAX_CHANNELS; ch++) { int clip_idx = CLIP_INDEX(scene_idx, ch); + if (clip_idx < 0 || clip_idx >= MAX_CLIPS) continue; Clip *clip = &engine->clips[clip_idx]; + if (!clip->buffer) continue; switch (clip->state) { case CLIP_EMPTY: clip->state = CLIP_RECORDING; @@ -642,7 +652,9 @@ void engine_redo(Engine *engine) { case ACTION_RESET_CLIP: { int clip_idx = action->index; + if (clip_idx < 0 || clip_idx >= MAX_CLIPS) break; Clip *clip = &engine->clips[clip_idx]; + if (!clip->buffer) break; clip->state = CLIP_EMPTY; clip->buffer_size = 0; clip->write_position = 0; diff --git a/engine.h b/engine.h index 1056c6a..88e3c88 100644 --- a/engine.h +++ b/engine.h @@ -45,9 +45,6 @@ typedef struct { // Size of the pre-allocated trigger pool (must match engine.c) #define QUEUED_TRIGGER_POOL_SIZE 64 -// Size of the pre-allocated trigger pool (must match engine.c) -#define QUEUED_TRIGGER_POOL_SIZE 64 - typedef enum { CMD_TRIGGER_CLIP, CMD_TRIGGER_SCENE, diff --git a/test_stress.c b/test_stress.c index 0cef2b0..cd4c0ea 100644 --- a/test_stress.c +++ b/test_stress.c @@ -5,15 +5,27 @@ #include #include #include +#include +#include +#include #include "engine.h" static volatile int keep_running = 1; +static jmp_buf segv_jmp; +static volatile int segv_caught = 0; static void handle_sigint(int sig) { (void)sig; keep_running = 0; } +// SIGSEGV handler to catch segfaults gracefully +static void handle_sigsegv(int sig) { + (void)sig; + segv_caught = 1; + longjmp(segv_jmp, 1); +} + static Engine *create_stress_engine(void) { Engine *engine = (Engine *)calloc(1, sizeof(Engine)); assert(engine != NULL); @@ -47,24 +59,43 @@ static Engine *create_stress_engine(void) { static void destroy_stress_engine(Engine *engine) { if (engine) { - engine->queued_triggers = NULL; // pool, no free + // Free queued triggers first + QueuedTrigger *qt = engine->queued_triggers; + while (qt) { + QueuedTrigger *next = qt->next; + free(qt); + qt = next; + } + engine->queued_triggers = NULL; + if (engine->transport) { transport_cleanup(engine->transport); free(engine->transport); + engine->transport = NULL; } for (int i = 0; i < MAX_CLIPS; i++) { free(engine->clips[i].buffer); + engine->clips[i].buffer = NULL; } free(engine); } } +// Random microsleep to expose race conditions +static void random_sleep_us(void) { + struct timespec ts = { + .tv_sec = 0, + .tv_nsec = (rand() % 10000) * 100 // 0-999 microseconds + }; + nanosleep(&ts, NULL); +} + // Stress test 1: Rapid clip triggers via command queue static void stress_command_queue(void) { printf("Stress test 1: Rapid command queue submission...\n"); Engine *engine = create_stress_engine(); - int num_ops = 10000; + int num_ops = 100000; int success = 0; int dropped = 0; for (int i = 0; i < num_ops; i++) { @@ -75,9 +106,13 @@ static void stress_command_queue(void) { } else { dropped++; } + + // Process commands periodically to prevent queue overflow + if (i % 100 == 0) { + engine_process_commands(engine); + } } - // At least some commands should have been accepted assert(success > 0); printf(" Submitted %d commands, %d succeeded, %d dropped\n", num_ops, success, dropped); @@ -99,7 +134,7 @@ static void stress_mixed_triggers(void) { printf("Stress test 2: Mixed clip/scene triggers...\n"); Engine *engine = create_stress_engine(); - int num_ops = 5000; + int num_ops = 50000; int success = 0; int dropped = 0; for (int i = 0; i < num_ops; i++) { @@ -116,6 +151,10 @@ static void stress_mixed_triggers(void) { } else { dropped++; } + + if (i % 50 == 0) { + engine_process_commands(engine); + } } assert(success > 0); @@ -123,7 +162,6 @@ static void stress_mixed_triggers(void) { engine_process_commands(engine); - // Verify queue drained CommandQueue *q = &engine->command_queue; assert(atomic_load(&q->write_index) == atomic_load(&q->read_index)); @@ -136,18 +174,16 @@ static void stress_queue_overflow(void) { printf("Stress test 3: Queue overflow (should drop gracefully)...\n"); Engine *engine = create_stress_engine(); - // Submit more than MAX_QUEUED_COMMANDS - int num_ops = MAX_QUEUED_COMMANDS * 2; + int num_ops = MAX_QUEUED_COMMANDS * 10; int dropped = 0; for (int i = 0; i < num_ops; i++) { int ret = engine_submit_command(engine, CMD_TRIGGER_CLIP, i % MAX_CLIPS, 0); if (ret != 0) dropped++; } - assert(dropped > 0); // some should have been dropped + assert(dropped > 0); printf(" Dropped %d commands (expected)\n", dropped); - // Process what we can engine_process_commands(engine); destroy_stress_engine(engine); @@ -159,19 +195,29 @@ static void stress_trigger_pool(void) { printf("Stress test 4: Trigger pool exhaustion...\n"); Engine *engine = create_stress_engine(); - // Set transport playing so queue_trigger is used engine->transport->state = TRANSPORT_PLAYING; atomic_store(&engine->transport->state_atomic, TRANSPORT_PLAYING); engine->quantize_mode = QUANTIZE_BEAT; atomic_store(&engine->quantize_mode_atomic, (int)QUANTIZE_BEAT); - // Queue many triggers (current implementation uses malloc, no pool limit) - int num_triggers = 200; + int num_triggers = 10000; for (int i = 0; i < num_triggers; i++) { queue_trigger(engine, i % MAX_CLIPS, false, 100); + + // Periodically process to prevent memory exhaustion + if (i % 1000 == 0) { + QueuedTrigger *qt = engine->queued_triggers; + int processed = 0; + while (qt && processed < 500) { + QueuedTrigger *next = qt->next; + free(qt); + qt = next; + processed++; + } + engine->queued_triggers = qt; + } } - // Verify that some triggers were queued (no crash) int count = 0; QueuedTrigger *qt = engine->queued_triggers; while (qt) { @@ -181,7 +227,6 @@ static void stress_trigger_pool(void) { assert(count > 0); printf(" Queued %d triggers\n", count); - // Clean up (free the malloc'd nodes) qt = engine->queued_triggers; while (qt) { QueuedTrigger *next = qt->next; @@ -199,7 +244,7 @@ static void stress_undo_redo(void) { printf("Stress test 5: Undo/Redo stress...\n"); Engine *engine = create_stress_engine(); - int num_ops = 1000; + int num_ops = 10000; int success = 0; int dropped = 0; for (int i = 0; i < num_ops; i++) { @@ -208,19 +253,26 @@ static void stress_undo_redo(void) { if (ret == 0) success++; else dropped++; engine_process_commands(engine); - // Undo every other operation if (i % 2 == 0) { ret = engine_submit_command(engine, CMD_UNDO, 0, 0); if (ret == 0) success++; else dropped++; engine_process_commands(engine); } + + // Rapid undo/redo cycles + if (i % 10 == 0) { + for (int j = 0; j < 5; j++) { + engine_submit_command(engine, CMD_UNDO, 0, 0); + engine_submit_command(engine, CMD_REDO, 0, 0); + } + engine_process_commands(engine); + } } assert(success > 0); printf(" Submitted %d commands, %d succeeded, %d dropped\n", num_ops * 2, success, dropped); - // Redo some - for (int i = 0; i < 100; i++) { + for (int i = 0; i < 1000; i++) { engine_submit_command(engine, CMD_REDO, 0, 0); engine_process_commands(engine); } @@ -234,7 +286,7 @@ static void stress_transport(void) { printf("Stress test 6: Transport state changes...\n"); Engine *engine = create_stress_engine(); - int num_ops = 5000; + int num_ops = 50000; for (int i = 0; i < num_ops; i++) { switch (i % 5) { case 0: @@ -253,9 +305,14 @@ static void stress_transport(void) { engine_submit_command(engine, CMD_SET_CLOCK_SOURCE, (int)CLOCK_SOURCE_MIDI, 0); break; } - engine_process_commands(engine); + + if (i % 10 == 0) { + engine_process_commands(engine); + } } + engine_process_commands(engine); + destroy_stress_engine(engine); printf(" PASSED\n"); } @@ -265,14 +322,23 @@ static void stress_quantize(void) { printf("Stress test 7: Quantize mode changes...\n"); Engine *engine = create_stress_engine(); - int num_ops = 5000; + int num_ops = 50000; for (int i = 0; i < num_ops; i++) { QuantizeMode mode = (QuantizeMode)(i % 3); engine_submit_command(engine, CMD_SET_QUANTIZE_MODE, (int)mode, 0); engine_submit_command(engine, CMD_SET_QUANTIZE_THRESHOLD, 0, (jack_nframes_t)(i * 100)); - engine_process_commands(engine); + + if (i % 5 == 0) { + engine_submit_command(engine, CMD_TRIGGER_CLIP, i % MAX_CLIPS, 0); + } + + if (i % 50 == 0) { + engine_process_commands(engine); + } } + engine_process_commands(engine); + destroy_stress_engine(engine); printf(" PASSED\n"); } @@ -282,35 +348,336 @@ static void stress_reset_while_triggering(void) { printf("Stress test 8: Reset clips while triggering...\n"); Engine *engine = create_stress_engine(); - int num_ops = 2000; + int num_ops = 10000; for (int i = 0; i < num_ops; i++) { int clip_idx = i % MAX_CLIPS; engine_submit_command(engine, CMD_TRIGGER_CLIP, clip_idx, 0); - engine_process_commands(engine); + + if (i % 3 == 0) { + engine_submit_command(engine, CMD_RESET_CLIP, clip_idx, 0); + } + + if (i % 5 == 0) { + engine_submit_command(engine, CMD_TRIGGER_SCENE, i % MAX_SCENES, 0); + } if (i % 10 == 0) { - engine_submit_command(engine, CMD_RESET_CLIP, clip_idx, 0); engine_process_commands(engine); } } + engine_process_commands(engine); + + destroy_stress_engine(engine); + printf(" PASSED\n"); +} + +// Stress test 9: Null pointer handling +static void stress_null_pointers(void) { + printf("Stress test 9: Null pointer handling...\n"); + + // Call all functions with NULL engine + engine_trigger_clip(NULL, 0); + engine_trigger_scene(NULL, 0); + engine_reset_clip(NULL, 0); + engine_set_quantize_mode(NULL, QUANTIZE_OFF); + engine_set_quantize_threshold(NULL, 0); + engine_transport_stop(NULL); + engine_transport_toggle_play(NULL); + engine_set_clock_source(NULL, CLOCK_SOURCE_INTERNAL); + engine_undo(NULL); + engine_redo(NULL); + engine_undo_action(NULL); + engine_redo_action(NULL); + engine_cleanup(NULL); + engine_process_commands(NULL); + queue_trigger(NULL, 0, false, 0); + + // Call with invalid indices + Engine *engine = create_stress_engine(); + engine_trigger_clip(engine, -1); + engine_trigger_clip(engine, MAX_CLIPS); + engine_trigger_scene(engine, -1); + engine_trigger_scene(engine, MAX_SCENES); + engine_reset_clip(engine, -1); + engine_reset_clip(engine, MAX_CLIPS); + + destroy_stress_engine(engine); + printf(" PASSED\n"); +} + +// Stress test 10: Memory corruption detection +static void stress_memory_corruption(void) { + printf("Stress test 10: Memory corruption detection...\n"); + + // Rapid create/destroy cycles + for (int i = 0; i < 1000; i++) { + Engine *temp = create_stress_engine(); + destroy_stress_engine(temp); + } + + // Stress with large allocations + for (int i = 0; i < 100; i++) { + float *big_buffer = (float *)calloc(MAX_BUFFER_SIZE * 10, sizeof(float)); + if (big_buffer) { + free(big_buffer); + } + } + + printf(" PASSED\n"); +} + +// Stress test 11: Concurrent command submission (multi-threaded) +typedef struct { + Engine *engine; + int thread_id; + int num_ops; + int *success; + int *dropped; +} ThreadArg; + +static void *thread_worker(void *arg) { + ThreadArg *targ = (ThreadArg *)arg; + Engine *engine = targ->engine; + int local_success = 0; + int local_dropped = 0; + + for (int i = 0; i < targ->num_ops; i++) { + int clip_idx = (targ->thread_id * 1000 + i) % MAX_CLIPS; + int ret = engine_submit_command(engine, CMD_TRIGGER_CLIP, clip_idx, 0); + if (ret == 0) { + local_success++; + } else { + local_dropped++; + } + + // Random sleep to increase chance of race conditions + if (i % 100 == 0) { + random_sleep_us(); + } + } + + *targ->success = local_success; + *targ->dropped = local_dropped; + return NULL; +} + +static void stress_concurrent(void) { + printf("Stress test 11: Concurrent command submission (multi-threaded)...\n"); + Engine *engine = create_stress_engine(); + + #define NUM_THREADS 8 + pthread_t threads[NUM_THREADS]; + ThreadArg args[NUM_THREADS]; + int successes[NUM_THREADS]; + int droppes[NUM_THREADS]; + + int ops_per_thread = 10000; + + for (int t = 0; t < NUM_THREADS; t++) { + args[t].engine = engine; + args[t].thread_id = t; + args[t].num_ops = ops_per_thread; + args[t].success = &successes[t]; + args[t].dropped = &droppes[t]; + pthread_create(&threads[t], NULL, thread_worker, &args[t]); + } + + // Process commands in main thread while workers submit + for (int i = 0; i < ops_per_thread * 2; i++) { + engine_process_commands(engine); + random_sleep_us(); + } + + for (int t = 0; t < NUM_THREADS; t++) { + pthread_join(threads[t], NULL); + } + + // Process remaining commands + engine_process_commands(engine); + + int total_success = 0; + int total_dropped = 0; + for (int t = 0; t < NUM_THREADS; t++) { + total_success += successes[t]; + total_dropped += droppes[t]; + } + + printf(" %d threads, %d ops each, %d succeeded, %d dropped\n", + NUM_THREADS, ops_per_thread, total_success, total_dropped); + + CommandQueue *q = &engine->command_queue; + assert(atomic_load(&q->write_index) >= atomic_load(&q->read_index)); + + destroy_stress_engine(engine); + printf(" PASSED\n"); +} + +// Stress test 12: Boundary conditions +static void stress_boundary(void) { + printf("Stress test 12: Boundary conditions...\n"); + Engine *engine = create_stress_engine(); + + // Test with transport in various states + for (int state = 0; state < 3; state++) { + engine->transport->state = (TransportState)state; + atomic_store(&engine->transport->state_atomic, state); + + // Submit commands while transport is in each state + for (int i = 0; i < 1000; i++) { + engine_submit_command(engine, CMD_TRIGGER_CLIP, i % MAX_CLIPS, 0); + engine_submit_command(engine, CMD_TRIGGER_SCENE, i % MAX_SCENES, 0); + engine_submit_command(engine, CMD_RESET_CLIP, i % MAX_CLIPS, 0); + } + engine_process_commands(engine); + } + + // Test with all quantize modes + for (int mode = 0; mode < 3; mode++) { + engine->quantize_mode = (QuantizeMode)mode; + atomic_store(&engine->quantize_mode_atomic, mode); + + for (int i = 0; i < 1000; i++) { + queue_trigger(engine, i % MAX_CLIPS, false, 100); + } + + // Clean up + QueuedTrigger *qt = engine->queued_triggers; + while (qt) { + QueuedTrigger *next = qt->next; + free(qt); + qt = next; + } + engine->queued_triggers = NULL; + } + destroy_stress_engine(engine); printf(" PASSED\n"); } +// Stress test 13: Rapid create/destroy with operations +static void stress_create_destroy(void) { + printf("Stress test 13: Rapid create/destroy with operations...\n"); + + for (int i = 0; i < 500; i++) { + Engine *engine = create_stress_engine(); + + // Do some operations + for (int j = 0; j < 100; j++) { + engine_submit_command(engine, CMD_TRIGGER_CLIP, j % MAX_CLIPS, 0); + engine_submit_command(engine, CMD_TRANSPORT_TOGGLE_PLAY, 0, 0); + engine_submit_command(engine, CMD_SET_QUANTIZE_MODE, j % 3, 0); + } + engine_process_commands(engine); + + // Queue some triggers + for (int j = 0; j < 50; j++) { + queue_trigger(engine, j % MAX_CLIPS, false, 100); + } + + destroy_stress_engine(engine); + } + + printf(" PASSED\n"); +} + int main(void) { signal(SIGINT, handle_sigint); + signal(SIGSEGV, handle_sigsegv); - printf("Running JACK Looper stress tests...\n\n"); + printf("Running JACK Looper stress tests (nuclear grade)...\n\n"); - stress_command_queue(); - stress_mixed_triggers(); - stress_queue_overflow(); - stress_trigger_pool(); - stress_undo_redo(); - stress_transport(); - stress_quantize(); - stress_reset_while_triggering(); + // Set up segfault protection + if (setjmp(segv_jmp) == 0) { + stress_command_queue(); + } else { + printf(" SEGFAULT CAUGHT in stress_command_queue!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_mixed_triggers(); + } else { + printf(" SEGFAULT CAUGHT in stress_mixed_triggers!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_queue_overflow(); + } else { + printf(" SEGFAULT CAUGHT in stress_queue_overflow!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_trigger_pool(); + } else { + printf(" SEGFAULT CAUGHT in stress_trigger_pool!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_undo_redo(); + } else { + printf(" SEGFAULT CAUGHT in stress_undo_redo!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_transport(); + } else { + printf(" SEGFAULT CAUGHT in stress_transport!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_quantize(); + } else { + printf(" SEGFAULT CAUGHT in stress_quantize!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_reset_while_triggering(); + } else { + printf(" SEGFAULT CAUGHT in stress_reset_while_triggering!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_null_pointers(); + } else { + printf(" SEGFAULT CAUGHT in stress_null_pointers!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_memory_corruption(); + } else { + printf(" SEGFAULT CAUGHT in stress_memory_corruption!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_concurrent(); + } else { + printf(" SEGFAULT CAUGHT in stress_concurrent!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_boundary(); + } else { + printf(" SEGFAULT CAUGHT in stress_boundary!\n"); + return 1; + } + + if (setjmp(segv_jmp) == 0) { + stress_create_destroy(); + } else { + printf(" SEGFAULT CAUGHT in stress_create_destroy!\n"); + return 1; + } printf("\nAll stress tests passed!\n"); return 0;