test: add automated state verification to integration tests
Co-authored-by: aider (deepseek/deepseek-reasoner) <aider@aider.chat>
This commit is contained in:
10
src/main.c
10
src/main.c
@@ -2,6 +2,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <signal.h>
|
||||||
#include <jack/jack.h>
|
#include <jack/jack.h>
|
||||||
#include <jack/midiport.h>
|
#include <jack/midiport.h>
|
||||||
|
|
||||||
@@ -101,6 +102,12 @@ static void jack_shutdown(void *arg)
|
|||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void sigusr1_handler(int signo) {
|
||||||
|
(void)signo;
|
||||||
|
int code = (int)current_state + 1;
|
||||||
|
_exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
(void)argc;
|
(void)argc;
|
||||||
@@ -151,6 +158,9 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
fprintf(stderr, "looper running (client name '%s')\n", client_name);
|
fprintf(stderr, "looper running (client name '%s')\n", client_name);
|
||||||
|
|
||||||
|
/* allow SIGUSR1 to report state and exit */
|
||||||
|
signal(SIGUSR1, sigusr1_handler);
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
sleep(1);
|
sleep(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,23 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
|
#include <sys/types.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Integration test for the JACK looper.
|
* Integration test for the JACK looper.
|
||||||
*
|
*
|
||||||
* 1. Start the looper binary (./looper)
|
* Uses SIGUSR1 to request the looper to report its current state and exit.
|
||||||
* 2. Wait for JACK ports to appear
|
* Verifies that MIDI note‑on and clock messages produce correct state transitions.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#define STATE_IDLE 0
|
||||||
|
#define STATE_RECORD 1
|
||||||
|
#define STATE_LOOPING 2
|
||||||
|
#define STATE_PAUSED 3
|
||||||
|
|
||||||
#define WAIT_SECONDS 1
|
#define WAIT_SECONDS 1
|
||||||
|
|
||||||
static int run_cmd(const char *fmt, ...) {
|
static int run_cmd(const char *fmt, ...) {
|
||||||
@@ -31,6 +31,155 @@ static int run_cmd(const char *fmt, ...) {
|
|||||||
return system(buf);
|
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) {
|
int main(void) {
|
||||||
/* 1. binary must exist */
|
/* 1. binary must exist */
|
||||||
if (system("test -x ./looper") != 0) {
|
if (system("test -x ./looper") != 0) {
|
||||||
@@ -38,85 +187,23 @@ int main(void) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. start the looper */
|
/* 2. check required external tool */
|
||||||
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 */
|
|
||||||
if (system("which jack_midi_send >/dev/null 2>&1") != 0) {
|
if (system("which jack_midi_send >/dev/null 2>&1") != 0) {
|
||||||
fprintf(stderr, "FATAL: jack_midi_send not available (install jack‑tools)\n");
|
fprintf(stderr, "FATAL: jack_midi_send not available\n");
|
||||||
kill(pid, SIGTERM);
|
|
||||||
waitpid(pid, NULL, 0);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 3. send note‑on messages (toggle state machine) */
|
/* 3. note-on toggle sequence */
|
||||||
printf("Sending first note‑on → IDLE → RECORD\n");
|
test_transition("IDLE -> RECORD", "control", "90 01 7f", STATE_RECORD);
|
||||||
run_cmd("jack_midi_send -c looper:control -m '90 01 7f' 2>/dev/null || echo 'jack_midi_send not available'");
|
test_transition("RECORD -> LOOPING", "control", "90 01 7f", STATE_LOOPING);
|
||||||
sleep(WAIT_SECONDS);
|
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");
|
/* 4. MIDI clock messages */
|
||||||
run_cmd("jack_midi_send -c looper:control -m '90 01 7f' 2>/dev/null || echo 'jack_midi_send not available'");
|
test_clock_start();
|
||||||
sleep(WAIT_SECONDS);
|
test_clock_stop();
|
||||||
|
test_clock_continue();
|
||||||
|
|
||||||
printf("Sending third note‑on → LOOPING → PAUSED\n");
|
printf("All tests passed!\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");
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user