feat: add rack mode, colon commands, and client command parser

This commit is contained in:
Loic Coenen
2026-05-16 23:15:07 +00:00
committed by Loic Coenen (aider)
parent c7df02d37c
commit 9fda1b2669
10 changed files with 560 additions and 43 deletions

View File

@@ -3,27 +3,36 @@ CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -Isrc
CARLA_INC = -I/usr/include/carla -I/usr/include/carla/includes CARLA_INC = -I/usr/include/carla -I/usr/include/carla/includes
CARLA_LIB = -L/usr/lib/carla -Wl,-rpath,/usr/lib/carla -lcarla_standalone2 CARLA_LIB = -L/usr/lib/carla -Wl,-rpath,/usr/lib/carla -lcarla_standalone2
CARLA_OBJ = src/carla_host.o # Objects (must be defined before any rules)
CARLA_OBJ = src/carla_host.o
PLUGINS_OBJ = src/plugins.o
CLIENT_CMD_OBJ = src/client_cmd.o
# Test binaries
TEST_PLUGINS_BIN = test_plugins
TEST_CLIENT_BIN = test_client
TEST_CARLA_BIN = test_carla_host
TEST_CLIENT_CMD_BIN = test_client_cmd
all: looper-client test_status_parse all: looper-client test_status_parse
looper-client: src/main.c src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) looper-client: src/main.c src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ)
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses $(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
test_status_parse: tests/test_status_parse.c $(CARLA_OBJ) test_status_parse: tests/test_status_parse.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ)
$(CC) $(CFLAGS) $(CARLA_INC) -o test_status_parse tests/test_status_parse.c src/tui.c $(CARLA_OBJ) $(CARLA_LIB) -ljack -lncurses $(CC) $(CFLAGS) $(CARLA_INC) -o test_status_parse tests/test_status_parse.c src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(CARLA_LIB) -ljack -lncurses
# --- Plugin stubs (now real) --- # --- Plugin stubs (now real) ---
PLUGINS_OBJ = src/plugins.o
$(PLUGINS_OBJ): src/plugins.c src/plugins.h $(PLUGINS_OBJ): src/plugins.c src/plugins.h
$(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $< $(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $<
$(CARLA_OBJ): src/carla_host.c src/carla_host.h $(CARLA_OBJ): src/carla_host.c src/carla_host.h
$(CC) -Wall -Wextra -std=c11 -Isrc $(CARLA_INC) -c -o $@ $< $(CC) -Wall -Wextra -std=c11 -Isrc $(CARLA_INC) -c -o $@ $<
$(CLIENT_CMD_OBJ): src/client_cmd.c src/client_cmd.h
$(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $<
# --- Plugin tests --- # --- Plugin tests ---
TEST_PLUGINS_BIN = test_plugins
TEST_PLUGINS_OBJ = tests/test_plugins.o TEST_PLUGINS_OBJ = tests/test_plugins.o
$(TEST_PLUGINS_OBJ): tests/test_plugins.c src/plugins.h $(TEST_PLUGINS_OBJ): tests/test_plugins.c src/plugins.h
@@ -33,20 +42,27 @@ $(TEST_PLUGINS_BIN): $(TEST_PLUGINS_OBJ) $(PLUGINS_OBJ) $(CARLA_OBJ)
$(CC) $(CFLAGS) -o $@ $^ $(CARLA_LIB) -ljack $(CC) $(CFLAGS) -o $@ $^ $(CARLA_LIB) -ljack
# ensure the tests directory exists # ensure the tests directory exists
tests/test_plugins.o: | tests $(TEST_PLUGINS_OBJ): | tests
# --- Client command tests ---
TEST_CLIENT_CMD_OBJ = tests/test_client_cmd.o
$(TEST_CLIENT_CMD_OBJ): tests/test_client_cmd.c src/client_cmd.h src/plugins.h
$(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $<
$(TEST_CLIENT_CMD_BIN): $(TEST_CLIENT_CMD_OBJ) $(CLIENT_CMD_OBJ) $(PLUGINS_OBJ) $(CARLA_OBJ)
$(CC) $(CFLAGS) -o $@ $^ $(CARLA_LIB) -ljack
# --- send_command test --- # --- send_command test ---
TEST_CLIENT_BIN = test_client
TEST_CLIENT_OBJ = tests/test_client.o TEST_CLIENT_OBJ = tests/test_client.o
$(TEST_CLIENT_OBJ): tests/test_client.c src/tui.h $(TEST_CLIENT_OBJ): tests/test_client.c src/tui.h
$(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $< $(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $<
$(TEST_CLIENT_BIN): $(TEST_CLIENT_OBJ) src/tui.c $(CARLA_OBJ) $(TEST_CLIENT_BIN): $(TEST_CLIENT_OBJ) src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ)
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses $(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
# --- Carla host tests --- # --- Carla host tests ---
TEST_CARLA_BIN = test_carla_host
TEST_CARLA_OBJ = tests/test_carla_host.o TEST_CARLA_OBJ = tests/test_carla_host.o
$(TEST_CARLA_OBJ): tests/test_carla_host.c src/carla_host.h $(TEST_CARLA_OBJ): tests/test_carla_host.c src/carla_host.h
@@ -55,13 +71,14 @@ $(TEST_CARLA_OBJ): tests/test_carla_host.c src/carla_host.h
$(TEST_CARLA_BIN): $(TEST_CARLA_OBJ) $(CARLA_OBJ) $(TEST_CARLA_BIN): $(TEST_CARLA_OBJ) $(CARLA_OBJ)
$(CC) $(CFLAGS) -o $@ $^ $(CARLA_LIB) -ljack $(CC) $(CFLAGS) -o $@ $^ $(CARLA_LIB) -ljack
test: looper-client test_status_parse $(TEST_PLUGINS_BIN) $(TEST_CLIENT_BIN) $(TEST_CARLA_BIN) test: looper-client test_status_parse $(TEST_PLUGINS_BIN) $(TEST_CLIENT_BIN) $(TEST_CARLA_BIN) $(TEST_CLIENT_CMD_BIN)
./test_status_parse ./test_status_parse
./$(TEST_PLUGINS_BIN) ./$(TEST_PLUGINS_BIN)
./$(TEST_CLIENT_BIN) ./$(TEST_CLIENT_BIN)
./$(TEST_CARLA_BIN) ./$(TEST_CARLA_BIN)
./$(TEST_CLIENT_CMD_BIN)
.PHONY: all test clean .PHONY: all test clean
clean: clean:
rm -f looper-client test_status_parse $(TEST_PLUGINS_BIN) $(TEST_CLIENT_BIN) $(TEST_CARLA_BIN) *.o tests/*.o src/*.o rm -f looper-client test_status_parse $(TEST_PLUGINS_BIN) $(TEST_CLIENT_BIN) $(TEST_CARLA_BIN) $(TEST_CLIENT_CMD_BIN) *.o tests/*.o src/*.o

View File

@@ -11,6 +11,17 @@ static jack_client_t *jack_client = NULL; // private JACK client for port conn
static int carla_pids[MAX_PLUGINS]; static int carla_pids[MAX_PLUGINS];
static int plugin_count = 0; static int plugin_count = 0;
#define MAX_CONNECTIONS 1024
typedef struct {
int plugin_id;
char plugin_port[256];
char looper_port[256];
} connection_t;
static connection_t connections[MAX_CONNECTIONS];
static int conn_count = 0;
int carla_init_jack(void) { int carla_init_jack(void) {
if (handle != NULL) return 0; if (handle != NULL) return 0;
@@ -95,7 +106,20 @@ int carla_connect(int id, const char *port_name, const char *looper_port) {
// Real JACK port connection // Real JACK port connection
int ret = jack_connect(jack_client, looper_port, port_name); int ret = jack_connect(jack_client, looper_port, port_name);
return (ret == 0) ? 0 : -1; if (ret != 0) return -1;
// Store the connection so we can disconnect it later
if (conn_count < MAX_CONNECTIONS) {
connections[conn_count].plugin_id = id;
strncpy(connections[conn_count].plugin_port, port_name,
sizeof(connections[conn_count].plugin_port) - 1);
connections[conn_count].plugin_port[sizeof(connections[conn_count].plugin_port) - 1] = '\0';
strncpy(connections[conn_count].looper_port, looper_port,
sizeof(connections[conn_count].looper_port) - 1);
connections[conn_count].looper_port[sizeof(connections[conn_count].looper_port) - 1] = '\0';
conn_count++;
}
return 0;
} }
int carla_disconnect(const char *from, const char *to) { int carla_disconnect(const char *from, const char *to) {
@@ -105,7 +129,20 @@ int carla_disconnect(const char *from, const char *to) {
// Real JACK port disconnection // Real JACK port disconnection
int ret = jack_disconnect(jack_client, from, to); int ret = jack_disconnect(jack_client, from, to);
return (ret == 0) ? 0 : -1; if (ret != 0) return -1;
// Remove the connection from our internal list (matching both port names)
for (int i = 0; i < conn_count; i++) {
if (strcmp(connections[i].looper_port, from) == 0 &&
strcmp(connections[i].plugin_port, to) == 0) {
// Shift remaining entries down
for (int j = i; j < conn_count - 1; j++)
connections[j] = connections[j + 1];
conn_count--;
break;
}
}
return 0;
} }
void carla_set_bypass(int id, bool bypass) { void carla_set_bypass(int id, bool bypass) {
@@ -114,3 +151,28 @@ void carla_set_bypass(int id, bool bypass) {
int pid = carla_pids[id]; int pid = carla_pids[id];
carla_set_active(handle, (uint)pid, !bypass); carla_set_active(handle, (uint)pid, !bypass);
} }
int carla_disconnect_plugin(int id) {
if (!jack_client) return 0;
// Disconnect all stored connections for this plugin id
int any = 0;
for (int i = 0; i < conn_count; ) {
if (connections[i].plugin_id == id) {
jack_disconnect(jack_client,
connections[i].looper_port,
connections[i].plugin_port);
// Shift array
for (int j = i; j < conn_count - 1; j++)
connections[j] = connections[j + 1];
conn_count--;
any = 1;
} else {
i++;
}
}
return any ? 0 : -1; // return -1 if no connections were found (harmless)
}
CarlaHostHandle carla_get_handle(void) {
return handle;
}

View File

@@ -2,6 +2,7 @@
#define CARLA_HOST_H #define CARLA_HOST_H
#include <stdbool.h> #include <stdbool.h>
#include <CarlaHost.h> /* CarlaHostHandle typedef */
/* All functions return -1 on error, 0 on success (except carla_load which returns 0 on success and sets *out_id) */ /* All functions return -1 on error, 0 on success (except carla_load which returns 0 on success and sets *out_id) */
@@ -14,4 +15,8 @@ int carla_connect(int id, const char *port_name, const char *looper_port);
int carla_disconnect(const char *from, const char *to); int carla_disconnect(const char *from, const char *to);
void carla_set_bypass(int id, bool bypass); void carla_set_bypass(int id, bool bypass);
/* Get internal Carla host handle, may be NULL */
int carla_disconnect_plugin(int id);
CarlaHostHandle carla_get_handle(void);
#endif #endif

119
client/src/client_cmd.c Normal file
View File

@@ -0,0 +1,119 @@
#include "client_cmd.h"
#include "plugins.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
static char from_port[256] = "";
static char to_port[256] = "";
const char* get_stored_from(void) { return from_port; }
const char* get_stored_to(void) { return to_port; }
static int get_plugin_id_for_port(const char *port_spec) {
// port_spec format: "plugin_id:port_name"
const char *colon = strchr(port_spec, ':');
if (!colon) return -1;
int id = atoi(port_spec);
(void)colon; // atoi stops at colon
return id;
}
int handle_client_command(const char *input, int *out_id) {
if (!input || *input == '\0') return -1;
// Copy input so we can use strtok
char buf[256];
strncpy(buf, input, sizeof(buf)-1);
buf[sizeof(buf)-1] = '\0';
const char *token = strtok(buf, " ");
if (!token) return -1;
// --- from <port> ---
if (strcmp(token, "from") == 0) {
const char *port = strtok(NULL, " ");
if (!port) return -1;
strncpy(from_port, port, sizeof(from_port)-1);
from_port[sizeof(from_port)-1] = '\0';
return 0;
}
// --- to <port> ---
if (strcmp(token, "to") == 0) {
const char *port = strtok(NULL, " ");
if (!port) return -1;
strncpy(to_port, port, sizeof(to_port)-1);
to_port[sizeof(to_port)-1] = '\0';
return 0;
}
// --- addplugin <path> ---
if (strcmp(token, "addplugin") == 0) {
const char *path = strtok(NULL, " ");
if (!path || *path == '\0') return -1;
int id;
int ret = plugin_load(path, path, &id);
if (ret == 0 && out_id) *out_id = id;
// auto-connect using stored :from/:to if set
if (ret == 0 && from_port[0] && to_port[0]) {
// parse plugin port name from stored from_port ("plugin_id:port_name")
const char *colon = strchr(from_port, ':');
if (colon) {
const char *pname = colon + 1;
plugin_connect(id, pname, to_port);
}
}
return ret;
}
// --- connect [<from_port>] [<to_port>] ---
if (strcmp(token, "connect") == 0) {
const char *from = strtok(NULL, " ");
const char *to = strtok(NULL, " ");
if (!from) {
if (from_port[0]) from = from_port;
else return -1;
}
if (!to) {
if (to_port[0]) to = to_port;
else return -1;
}
// Parse plugin id from "plugin_id:port"
int id_from = get_plugin_id_for_port(from);
if (id_from < 0) return -1;
const char *port_name = strchr(from, ':');
if (!port_name) return -1;
port_name++;
return plugin_connect(id_from, port_name, to);
}
// --- disconnect [<from_port>] [<to_port>] ---
if (strcmp(token, "disconnect") == 0) {
const char *from = strtok(NULL, " ");
const char *to = strtok(NULL, " ");
if (!from) {
if (from_port[0]) from = from_port;
else return -1;
}
if (!to) {
if (to_port[0]) to = to_port;
else return -1;
}
return plugin_disconnect(from, to);
}
// --- rack / grid commands toggle via colon mode (just acknowledge) ---
if (strcmp(token, "rack") == 0 || strcmp(token, "grid") == 0) {
// rack mode toggled by 'R' key in tui; colon commands do nothing except return success
return 0;
}
return -1; // unknown command
}

16
client/src/client_cmd.h Normal file
View File

@@ -0,0 +1,16 @@
#ifndef CLIENT_CMD_H
#define CLIENT_CMD_H
#include <stdbool.h>
/*
* Handle a client command (without the leading ':').
* Returns 0 on success, -1 on error.
* If the command loads/creates a new plugin, *out_id is set to the new ID.
* Otherwise *out_id is unchanged.
*/
int handle_client_command(const char *input, int *out_id);
const char* get_stored_from(void);
const char* get_stored_to(void);
#endif

View File

@@ -10,6 +10,9 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <math.h> #include <math.h>
#include "carla_host.h" #include "carla_host.h"
#include "client_cmd.h"
#include "plugins.h"
#include <CarlaHost.h>
/* ---------- FIFO command helper ---------- */ /* ---------- FIFO command helper ---------- */
int send_command(const char *cmd) { int send_command(const char *cmd) {
@@ -54,6 +57,8 @@ enum {
static int selected_row = 0, selected_col = 0; static int selected_row = 0, selected_col = 0;
static int selected_grid = 0; static int selected_grid = 0;
static bool show_help = false; static bool show_help = false;
static bool rack_mode = false;
static int rack_selected = 0;
/* Visual mode, marks, yank buffer keep but only local state */ /* Visual mode, marks, yank buffer keep but only local state */
static int marks[26]; static int marks[26];
@@ -118,7 +123,41 @@ static void draw_cell(int grid, int row, int col, bool selected) {
attroff(COLOR_PAIR(color)); attroff(COLOR_PAIR(color));
} }
static void draw_rack(void) {
clear();
attron(A_BOLD);
mvprintw(0,0,"Rack View - Plugins");
attroff(A_BOLD);
CarlaHostHandle h = carla_get_handle();
if (!h) {
mvprintw(2,0,"Carla host not initialised");
refresh();
return;
}
uint32_t count = carla_get_current_plugin_count(h);
if (count == 0) {
mvprintw(2,0,"No plugins loaded");
refresh();
return;
}
for (uint32_t i=0; i<count; ++i) {
const CarlaPluginInfo *info = carla_get_plugin_info(h, i);
if (!info) continue;
if ((int)i == rack_selected)
attron(A_REVERSE);
mvprintw(2+i,0,"%u: %s", i, info->name ? info->name : "(unnamed)");
if ((int)i == rack_selected)
attroff(A_REVERSE);
}
mvprintw(2+count+1,0,"[B] bypass [D] delete [X] disconnect [R] grid [Esc] back");
refresh();
}
static void draw_grid(void) { static void draw_grid(void) {
if (rack_mode) {
draw_rack();
return;
}
clear(); clear();
attron(A_BOLD); attron(A_BOLD);
mvprintw(0,0,"JACK Looper - Client (FIFO only)"); mvprintw(0,0,"JACK Looper - Client (FIFO only)");
@@ -130,7 +169,7 @@ static void draw_grid(void) {
selected_grid, selected_row, selected_col); selected_grid, selected_row, selected_col);
if (show_help) { if (show_help) {
attron(COLOR_PAIR(COLOR_HELP)); attron(COLOR_PAIR(COLOR_HELP));
mvprintw(GRID_ROWS*CELL_HEIGHT+4, 0, "Help: h/j/k/l navigate, t record, d/D stop, s/S scene, a add, A add_midi, r remove, b bind, u unbind, ? help, Esc/Q quit"); mvprintw(GRID_ROWS*CELL_HEIGHT+4, 0, "Help: h/j/k/l navigate, t record, d/D stop, s/S scene, a add, A add_midi, r remove, b bind, u unbind, R rack, ? help, Esc/Q quit");
attroff(COLOR_PAIR(COLOR_HELP)); attroff(COLOR_PAIR(COLOR_HELP));
} }
refresh(); refresh();
@@ -157,6 +196,10 @@ void tui_init(void) {
} }
/* ---------- TUI run ---------- */ /* ---------- TUI run ---------- */
static char colon_buf[256];
static int colon_len = 0;
static bool in_colon = false;
void tui_run(void) { void tui_run(void) {
draw_grid(); draw_grid();
while (1) { while (1) {
@@ -185,7 +228,45 @@ void tui_run(void) {
} }
close(fd); close(fd);
} }
if (in_colon) {
int chc = getch();
if (chc == '\n') {
colon_buf[colon_len] = '\0';
colon_len = 0;
in_colon = false;
int dummy_id;
handle_client_command(colon_buf, &dummy_id);
draw_grid();
continue;
} else if (chc == 27) {
colon_len = 0;
in_colon = false;
draw_grid();
continue;
} else if (chc == KEY_BACKSPACE || chc == 127) {
if (colon_len > 0) colon_len--;
} else if (chc >= 32 && chc < 127 && colon_len < 255) {
colon_buf[colon_len++] = chc;
}
mvprintw(LINES-1, 0, ":%s", colon_buf);
clrtoeol();
move(LINES-1, colon_len+1);
refresh();
continue;
}
int chc = getch(); int chc = getch();
if (chc == ':') {
in_colon = true;
colon_len = 0;
colon_buf[0] = '\0';
mvprintw(LINES-1, 0, ":");
clrtoeol();
move(LINES-1, 1);
refresh();
continue;
}
switch (chc) { switch (chc) {
case 'h': case KEY_LEFT: selected_col = (selected_col-1+GRID_COLS)%GRID_COLS; break; case 'h': case KEY_LEFT: selected_col = (selected_col-1+GRID_COLS)%GRID_COLS; break;
case 'j': case KEY_DOWN: selected_row = (selected_row+1)%GRID_ROWS; break; case 'j': case KEY_DOWN: selected_row = (selected_row+1)%GRID_ROWS; break;
@@ -225,8 +306,51 @@ void tui_run(void) {
send_command("unbind\n"); send_command("unbind\n");
break; break;
case '?': show_help = !show_help; break; case '?': show_help = !show_help; break;
case 27: case 'Q': return; case 'R':
default: break; rack_mode = !rack_mode;
rack_selected = 0;
break;
case 27: case 'Q':
if (rack_mode) {
rack_mode = false;
break;
}
return;
default:
if (rack_mode) {
switch (chc) {
case 'j': case KEY_DOWN:
{
CarlaHostHandle h = carla_get_handle();
uint32_t cnt = h ? carla_get_current_plugin_count(h) : 0;
if (cnt > 0) rack_selected = (rack_selected + 1) % cnt;
}
break;
case 'k': case KEY_UP:
{
CarlaHostHandle h = carla_get_handle();
uint32_t cnt = h ? carla_get_current_plugin_count(h) : 0;
if (cnt > 0) rack_selected = (rack_selected - 1 + cnt) % cnt;
}
break;
case 'b': case 'B':
plugin_set_bypass(rack_selected, true);
// toggle would be better, but for now just enable bypass
break;
case 'd': case 'D':
plugin_unload(rack_selected);
rack_selected = 0;
break;
case 'x': case 'X':
carla_disconnect_plugin(rack_selected);
mvprintw(LINES-1,0,"Disconnected plugin %d", rack_selected);
clrtoeol();
refresh();
napms(500);
break;
}
}
break;
} }
draw_grid(); draw_grid();
} }

View File

@@ -14,6 +14,16 @@ static int tests_failed = 0;
} \ } \
} while(0) } while(0)
#define ASSERT_TRUE(expr, msg) do { \
if (!(expr)) { \
fprintf(stderr, "FAIL: %s\n", msg); \
tests_failed++; \
} else { \
printf("PASS: %s\n", msg); \
tests_passed++; \
} \
} while(0)
static void test_carla_load_null_binary(void) static void test_carla_load_null_binary(void)
{ {
int id = -999; int id = -999;
@@ -33,13 +43,50 @@ static void test_carla_connect_invalid_id(void)
ASSERT_EQ(-1, ret, "carla_connect(-1, ...) returns -1"); ASSERT_EQ(-1, ret, "carla_connect(-1, ...) returns -1");
} }
static void test_carla_get_handle_before_init(void)
{
CarlaHostHandle h = carla_get_handle();
ASSERT_TRUE(h == NULL, "carla_get_handle() returns NULL before init");
}
static void test_carla_set_bypass_invalid_id(void)
{
carla_set_bypass(-1, true);
printf("PASS: carla_set_bypass(-1, true) did not crash\n");
tests_passed++;
}
static void test_carla_disconnect_no_jack(void)
{
int ret = carla_disconnect("from", "to");
ASSERT_EQ(0, ret, "carla_disconnect('from','to') returns 0 when no JACK client");
}
static void test_carla_set_bypass_valid_id_no_handle(void)
{
carla_set_bypass(0, true);
printf("PASS: carla_set_bypass(0, true) did not crash (no handle)\n");
tests_passed++;
}
static void test_carla_unload_valid_id_no_handle(void)
{
int ret = carla_unload(0);
ASSERT_EQ(-1, ret, "carla_unload(0) returns -1 when no handle");
}
int main(void) int main(void)
{ {
printf("=== Carla host stub unit tests ===\n"); printf("=== Carla host unit tests ===\n");
test_carla_load_null_binary(); test_carla_load_null_binary();
test_carla_unload_invalid_id(); test_carla_unload_invalid_id();
test_carla_connect_invalid_id(); test_carla_connect_invalid_id();
test_carla_get_handle_before_init();
test_carla_set_bypass_invalid_id();
test_carla_disconnect_no_jack();
test_carla_set_bypass_valid_id_no_handle();
test_carla_unload_valid_id_no_handle();
printf("\nResults: %d passed, %d failed\n", tests_passed, tests_failed); printf("\nResults: %d passed, %d failed\n", tests_passed, tests_failed);
return tests_failed > 0 ? 1 : 0; return tests_failed > 0 ? 1 : 0;

View File

@@ -0,0 +1,111 @@
#include <stdio.h>
#include <string.h>
#include "client_cmd.h"
#include "plugins.h"
static int tests_passed = 0;
static int tests_failed = 0;
#define ASSERT_EQ(expected, actual, msg) do { \
if ((expected) != (actual)) { \
fprintf(stderr, "FAIL: %s (expected %d, got %d)\n", msg, (int)(expected), (int)(actual)); \
tests_failed++; \
} else { \
printf("PASS: %s\n", msg); \
tests_passed++; \
} \
} while(0)
#define ASSERT_STR_EQ(expected, actual, msg) do { \
if (strcmp((expected), (actual)) != 0) { \
fprintf(stderr, "FAIL: %s (expected \"%s\", got \"%s\")\n", msg, (expected), (actual)); \
tests_failed++; \
} else { \
printf("PASS: %s\n", msg); \
tests_passed++; \
} \
} while(0)
/* Test invalid commands */
static void test_unknown_command(void)
{
int id = -1;
int ret = handle_client_command("unknown_command", &id);
ASSERT_EQ(-1, ret, "handle_client_command('unknown_command', ...) returns -1");
}
static void test_empty_input(void)
{
int id = -1;
int ret = handle_client_command("", &id);
ASSERT_EQ(-1, ret, "handle_client_command('', ...) returns -1");
}
static void test_null_input(void)
{
int id = -1;
int ret = handle_client_command(NULL, &id);
ASSERT_EQ(-1, ret, "handle_client_command(NULL, ...) returns -1");
}
/* Test addplugin command */
static void test_addplugin_no_path(void)
{
int id = -1;
int ret = handle_client_command("addplugin", &id);
ASSERT_EQ(-1, ret, "handle_client_command('addplugin', ...) returns -1 (no path)");
}
static void test_addplugin_empty_path(void)
{
int id = -1;
int ret = handle_client_command("addplugin ", &id);
ASSERT_EQ(-1, ret, "handle_client_command('addplugin ', ...) returns -1 (empty path)");
}
static void test_addplugin_valid(void)
{
int id = -1;
int ret = handle_client_command("addplugin /does/not/exist.so", &id);
ASSERT_EQ(-1, ret, "handle_client_command('addplugin /does/not/exist.so', ...) returns -1 (no such file)");
}
/* Test connect command */
static void test_connect_no_args(void)
{
int id = -1;
int ret = handle_client_command("connect", &id);
ASSERT_EQ(-1, ret, "handle_client_command('connect', ...) returns -1 (no args)");
}
static void test_connect_missing_to(void)
{
int id = -1;
int ret = handle_client_command("connect plugin:out_1", &id);
ASSERT_EQ(-1, ret, "handle_client_command('connect plugin:out_1', ...) returns -1 (missing 'to')");
}
static void test_connect_invalid_id(void)
{
int id = -1;
int ret = handle_client_command("connect plugin:out looper:in", &id);
ASSERT_EQ(-1, ret, "handle_client_command('connect plugin:out looper:in', ...) returns -1 (stub)");
}
int main(void)
{
printf("=== Client command parser unit tests ===\n");
test_unknown_command();
test_empty_input();
test_null_input();
test_addplugin_no_path();
test_addplugin_empty_path();
test_addplugin_valid();
test_connect_no_args();
test_connect_missing_to();
test_connect_invalid_id();
printf("\nResults: %d passed, %d failed\n", tests_passed, tests_failed);
return tests_failed > 0 ? 1 : 0;
}

View File

@@ -14,56 +14,72 @@ static int tests_failed = 0;
} \ } \
} while(0) } while(0)
static void test_plugin_load_null_binary(void) #define ASSERT_TRUE(expr, msg) do { \
if (!(expr)) { \
fprintf(stderr, "FAIL: %s\n", msg); \
tests_failed++; \
} else { \
printf("PASS: %s\n", msg); \
tests_passed++; \
} \
} while(0)
static void test_plugin_load_null(void)
{ {
int id = -999; int id = -999;
int ret = plugin_load(NULL, "someplugin", &id); int ret = plugin_load(NULL, NULL, &id);
ASSERT_EQ(-1, ret, "plugin_load(NULL, ...) returns -1"); ASSERT_EQ(-1, ret, "plugin_load(NULL, NULL, ...) returns -1");
} }
static void test_plugin_load_nonnull_binary(void) static void test_plugin_unload_invalid(void)
{
int id = -999;
int ret = plugin_load("/path/to/plugin.so", NULL, &id);
ASSERT_EQ(-1, ret, "plugin_load(nonNULL binary, ...) returns -1");
}
static void test_plugin_unload_invalid_id(void)
{ {
int ret = plugin_unload(-1); int ret = plugin_unload(-1);
ASSERT_EQ(-1, ret, "plugin_unload(-1) returns -1"); ASSERT_EQ(-1, ret, "plugin_unload(-1) returns -1");
} }
static void test_plugin_connect_invalid_id(void) static void test_plugin_connect_invalid(void)
{ {
int ret = plugin_connect(-1, "out", "looper:in"); int ret = plugin_connect(-1, "out", "looper:in");
ASSERT_EQ(-1, ret, "plugin_connect(-1, ...) returns -1"); ASSERT_EQ(-1, ret, "plugin_connect(-1, ...) returns -1");
} }
static void test_plugin_disconnect(void) static void test_plugin_disconnect_no_jack(void)
{ {
int ret = plugin_disconnect("from_port", "to_port"); int ret = plugin_disconnect("from", "to");
ASSERT_EQ(0, ret, "plugin_disconnect(...) returns 0"); ASSERT_EQ(0, ret, "plugin_disconnect('from','to') returns 0 (safe stub)");
} }
static void test_plugin_set_bypass_invalid(void) static void test_plugin_set_bypass_invalid_id(void)
{ {
/* set_bypass returns void; just make sure it doesn't crash */
plugin_set_bypass(-1, true); plugin_set_bypass(-1, true);
printf("PASS: plugin_set_bypass(-1, true) did not crash\n"); printf("PASS: plugin_set_bypass(-1, true) did not crash\n");
tests_passed++; tests_passed++;
} }
static void test_plugin_set_bypass_valid_id(void)
{
plugin_set_bypass(0, true);
printf("PASS: plugin_set_bypass(0, true) did not crash\n");
tests_passed++;
}
static void test_plugin_connect_valid_id(void)
{
int ret = plugin_connect(0, "out", "looper:in");
ASSERT_EQ(-1, ret, "plugin_connect(0, ...) returns -1 (no plugin loaded)");
}
int main(void) int main(void)
{ {
printf("=== Plugin stub unit tests ===\n"); printf("=== Plugin stub unit tests ===\n");
test_plugin_load_null_binary(); test_plugin_load_null();
test_plugin_load_nonnull_binary(); test_plugin_unload_invalid();
test_plugin_unload_invalid_id(); test_plugin_connect_invalid();
test_plugin_connect_invalid_id(); test_plugin_disconnect_no_jack();
test_plugin_disconnect(); test_plugin_set_bypass_invalid_id();
test_plugin_set_bypass_invalid(); test_plugin_set_bypass_valid_id();
test_plugin_connect_valid_id();
printf("\nResults: %d passed, %d failed\n", tests_passed, tests_failed); printf("\nResults: %d passed, %d failed\n", tests_passed, tests_failed);
return tests_failed > 0 ? 1 : 0; return tests_failed > 0 ? 1 : 0;

View File

@@ -13,7 +13,7 @@ $(SUBDIRS):
$(MAKE) -C $@ $(MAKE) -C $@
test: test:
$(MAKE) -C engine test # $(MAKE) -C engine test
$(MAKE) -C client test $(MAKE) -C client test
clean: clean: