#include #include #include #include #include #include #include #include #include /* * Integration test for the JACK looper. * * Uses SIGUSR1 to request the looper to report its current state and exit. * Verifies that MIDI note‑on and clock messages produce correct state transitions. */ #define STATE_IDLE 0 #define STATE_RECORD 1 #define STATE_LOOPING 2 #define STATE_PAUSED 3 #define WAIT_SECONDS 1 static int run_cmd(const char *fmt, ...) { char buf[512]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); return system(buf); } /* * Start a fresh instance of the looper, wait for JACK ports to appear, * return its PID, or -1 on failure. */ static pid_t start_looper(void) { pid_t pid = fork(); if (pid < 0) { perror("fork"); return -1; } if (pid == 0) { /* child: suppress stderr messages */ close(2); open("/dev/null", O_WRONLY); execl("./looper", "looper", NULL); perror("execl"); _exit(1); } printf("Started looper (pid %d)\n", (int)pid); sleep(3); /* wait for JACK ports to register */ return pid; } /* * Send a hex MIDI message to the given port. */ static int send_midi(const char *port, const char *msg) { char cmd[512]; snprintf(cmd, sizeof(cmd), "jack_midi_send -c looper:%s -m '%s' 2>/dev/null", port, msg); int ret = system(cmd); if (ret != 0) { fprintf(stderr, "jack_midi_send failed: %s\n", cmd); } return ret; } /* * Ask the looper to report its current state and exit. * Returns the state (0..3) or -1 on failure. */ static int request_state_and_exit(pid_t pid) { kill(pid, SIGUSR1); int status; if (waitpid(pid, &status, 0) != pid) { perror("waitpid"); return -1; } if (WIFEXITED(status)) { int code = WEXITSTATUS(status); /* looper returns state+1, so state = code-1 */ int state = code - 1; if (state >= STATE_IDLE && state <= STATE_PAUSED) { return state; } fprintf(stderr, "Unexpected exit code %d (expected 1..4)\n", code); return -1; } fprintf(stderr, "Looper terminated by signal %d\n", WTERMSIG(status)); return -1; } /* * Perform a single transition test: start looper, send @p midi_msg * (may be NULL for idle‑only test), then verify state equals @p expected_state. * Exits the whole program on failure. */ static void test_transition(const char *label, const char *midi_port, const char *midi_msg, int expected_state) { printf("Test: %s (expect state %d)\n", label, expected_state); pid_t pid = start_looper(); if (pid < 0) { fprintf(stderr, "FAIL: could not start looper\n"); exit(1); } if (midi_msg) { send_midi(midi_port, midi_msg); sleep(WAIT_SECONDS); } int got = request_state_and_exit(pid); if (got == expected_state) { printf(" PASS\n"); } else { fprintf(stderr, " FAIL: got %d, expected %d\n", got, expected_state); exit(1); } } /* * Test MIDI clock Start (0xFA) while idle. */ static void test_clock_start(void) { test_transition("clock start -> record", "clock", "FA", STATE_RECORD); } /* * Test MIDI clock Stop (0xFC) after first entering RECORD via control note. */ static void test_clock_stop(void) { printf("Test: clock stop after record (expect idle)\n"); pid_t pid = start_looper(); if (pid < 0) exit(1); /* IDLE -> RECORD */ send_midi("control", "90 01 7f"); sleep(WAIT_SECONDS); /* clock stop -> IDLE */ send_midi("clock", "FC"); sleep(WAIT_SECONDS); int got = request_state_and_exit(pid); if (got == STATE_IDLE) { printf(" PASS\n"); } else { fprintf(stderr, " FAIL: got %d, expected %d\n", got, STATE_IDLE); exit(1); } } /* * Test MIDI clock Continue (0xFB) from PAUSED -> LOOPING. */ static void test_clock_continue(void) { printf("Test: clock continue from paused (expect looping)\n"); pid_t pid = start_looper(); if (pid < 0) exit(1); /* IDLE -> RECORD */ send_midi("control", "90 01 7f"); sleep(WAIT_SECONDS); /* RECORD -> LOOPING */ send_midi("control", "90 01 7f"); sleep(WAIT_SECONDS); /* LOOPING -> PAUSED */ send_midi("control", "90 01 7f"); sleep(WAIT_SECONDS); /* clock continue -> LOOPING */ send_midi("clock", "FB"); sleep(WAIT_SECONDS); int got = request_state_and_exit(pid); if (got == STATE_LOOPING) { printf(" PASS\n"); } else { fprintf(stderr, " FAIL: got %d, expected %d\n", got, STATE_LOOPING); exit(1); } } int main(void) { /* 1. binary must exist */ if (system("test -x ./looper") != 0) { fprintf(stderr, "FATAL: looper binary not found\n"); return 1; } /* 2. check required external tool */ if (system("which jack_midi_send >/dev/null 2>&1") != 0) { fprintf(stderr, "FATAL: jack_midi_send not available\n"); return 1; } /* 3. note-on toggle sequence */ test_transition("IDLE -> RECORD", "control", "90 01 7f", STATE_RECORD); test_transition("RECORD -> LOOPING", "control", "90 01 7f", STATE_LOOPING); test_transition("LOOPING -> PAUSED", "control", "90 01 7f", STATE_PAUSED); test_transition("PAUSED -> LOOPING", "control", "90 01 7f", STATE_LOOPING); /* 4. MIDI clock messages */ test_clock_start(); test_clock_stop(); test_clock_continue(); printf("All tests passed!\n"); return 0; }