diff --git a/src/main.c b/src/main.c index 04a07b7..933defa 100644 --- a/src/main.c +++ b/src/main.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -101,6 +102,12 @@ static void jack_shutdown(void *arg) exit(0); } +static void sigusr1_handler(int signo) { + (void)signo; + int code = (int)current_state + 1; + _exit(code); +} + int main(int argc, char *argv[]) { (void)argc; @@ -151,6 +158,9 @@ int main(int argc, char *argv[]) fprintf(stderr, "looper running (client name '%s')\n", client_name); + /* allow SIGUSR1 to report state and exit */ + signal(SIGUSR1, sigusr1_handler); + while (1) { sleep(1); } diff --git a/tests/integration.c b/tests/integration.c index 4877e81..c89fef3 100644 --- a/tests/integration.c +++ b/tests/integration.c @@ -3,23 +3,23 @@ #include #include #include +#include #include #include +#include /* * Integration test for the JACK looper. * - * 1. Start the looper binary (./looper) - * 2. Wait for JACK ports to appear - * 3. Send several note‑on messages (MIDI note number 1) to its 'control' port - * using jack_midi_send, testing the state machine: - * IDLE → RECORD → LOOPING → PAUSED → LOOPING → PAUSED (toggle) - * 4. Send a MIDI clock Start (0xFA) to force IDLE → RECORD. - * 5. Send a MIDI clock Stop (0xFC) to force RECORD → IDLE. - * 6. Send a MIDI clock Continue (0xFB) to resume LOOPING from PAUSED. - * 7. Kill the process and check that it exits cleanly. + * 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, ...) { @@ -31,6 +31,155 @@ static int run_cmd(const char *fmt, ...) { 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) { @@ -38,85 +187,23 @@ int main(void) { return 1; } - /* 2. start the looper */ - pid_t pid = fork(); - if (pid < 0) { - perror("fork"); - return 1; - } - if (pid == 0) { - execl("./looper", "looper", NULL); - perror("execl"); - _exit(1); - } - - printf("Waiting for looper (pid %d) to register ports...\n", (int)pid); - sleep(3); /* generous, JACK ports must be visible */ - - /* check jack_midi_send availability */ + /* 2. check required external tool */ if (system("which jack_midi_send >/dev/null 2>&1") != 0) { - fprintf(stderr, "FATAL: jack_midi_send not available (install jack‑tools)\n"); - kill(pid, SIGTERM); - waitpid(pid, NULL, 0); + fprintf(stderr, "FATAL: jack_midi_send not available\n"); return 1; } - /* 3. send note‑on messages (toggle state machine) */ - printf("Sending first note‑on → IDLE → RECORD\n"); - run_cmd("jack_midi_send -c looper:control -m '90 01 7f' 2>/dev/null || echo 'jack_midi_send not available'"); - sleep(WAIT_SECONDS); + /* 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); - printf("Sending second note‑on → RECORD → LOOPING\n"); - run_cmd("jack_midi_send -c looper:control -m '90 01 7f' 2>/dev/null || echo 'jack_midi_send not available'"); - sleep(WAIT_SECONDS); + /* 4. MIDI clock messages */ + test_clock_start(); + test_clock_stop(); + test_clock_continue(); - printf("Sending third note‑on → LOOPING → PAUSED\n"); - run_cmd("jack_midi_send -c looper:control -m '90 01 7f' 2>/dev/null || echo 'jack_midi_send not available'"); - sleep(WAIT_SECONDS); - - printf("Sending fourth note‑on → PAUSED → LOOPING\n"); - run_cmd("jack_midi_send -c looper:control -m '90 01 7f' 2>/dev/null || echo 'jack_midi_send not available'"); - sleep(WAIT_SECONDS); - - printf("Sending fifth note‑on → LOOPING → PAUSED (toggle)\n"); - run_cmd("jack_midi_send -c looper:control -m '90 01 7f' 2>/dev/null || echo 'jack_midi_send not available'"); - sleep(WAIT_SECONDS); - - /* 4. MIDI clock start (0xFA) – IDLE → RECORD */ - printf("Sending MIDI clock Start (0xFA)\n"); - run_cmd("jack_midi_send -c looper:clock -m 'FA' 2>/dev/null || echo 'jack_midi_send not available'"); - sleep(1); - - /* 5. MIDI clock stop (0xFC) – RECORD → IDLE */ - printf("Sending MIDI clock Stop (0xFC)\n"); - run_cmd("jack_midi_send -c looper:clock -m 'FC' 2>/dev/null || echo 'jack_midi_send not available'"); - sleep(1); - - /* bring back to PAUSED via control port */ - printf("Sending note‑on to reach PAUSED again\n"); - run_cmd("jack_midi_send -c looper:control -m '90 01 7f' 2>/dev/null || echo 'jack_midi_send not available'"); - sleep(1); - run_cmd("jack_midi_send -c looper:control -m '90 01 7f' 2>/dev/null || echo 'jack_midi_send not available'"); - sleep(1); - run_cmd("jack_midi_send -c looper:control -m '90 01 7f' 2>/dev/null || echo 'jack_midi_send not available'"); // now PAUSED - sleep(1); - - /* 6. MIDI clock Continue (0xFB) – PAUSED → LOOPING */ - printf("Sending MIDI clock Continue (0xFB)\n"); - run_cmd("jack_midi_send -c looper:clock -m 'FB' 2>/dev/null || echo 'jack_midi_send not available'"); - sleep(1); - - /* cleanup */ - printf("Terminating looper...\n"); - kill(pid, SIGTERM); - int status; - waitpid(pid, &status, 0); - if (WIFEXITED(status)) { - printf("Looper exited with status %d\n", WEXITSTATUS(status)); - } else if (WIFSIGNALED(status)) { - printf("Looper terminated by signal %d\n", WTERMSIG(status)); - } - - printf("Integration test finished (manual verification recommended)\n"); + printf("All tests passed!\n"); return 0; }