Merge branch '8-add-tui' - tests not passing
This commit is contained in:
136
client/PLAN.md
Normal file
136
client/PLAN.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Plan: Refactor TUI into Standalone FIFO‑Client Binary
|
||||
|
||||
## Goal
|
||||
Extract the TUI from the existing monolithic codebase into a separate `looper-client` binary that communicates with the engine **only** via the FIFO pipe (`/tmp/looper_cmd`).
|
||||
The TUI must **not** link to any engine source files (`engine.c`, `dispatcher.c`, `carla.c`, etc.) or use their headers beyond shared type definitions (e.g., `command.h`). All state is maintained by the engine; the TUI sends commands and assumes they succeed.
|
||||
|
||||
## Background
|
||||
- The looper engine runs as a separate JACK client and listens for commands on:
|
||||
- FIFO pipe (`/tmp/looper_cmd`) – text‑based commands
|
||||
- `looper:control` – MIDI note‑on events
|
||||
- The current TUI uses a local `Engine` / `AppState` / `dispatcher` that does **not** talk to the real looper. It was designed for unit testing.
|
||||
|
||||
## Task 1 – Create a new `client/` directory structure
|
||||
- Keep existing `client/src/tui.c` and `client/src/tui.h`.
|
||||
- Remove all `#include` directives that reference engine internal headers:
|
||||
- `engine.h`
|
||||
- `dispatcher.h`
|
||||
- `wav_io.h`
|
||||
- `transport.h`
|
||||
- `carla.h`
|
||||
- Remove any `Engine*`, `AppState`, `DispatchFn`, `Clip`, `MidiClip` usage.
|
||||
- **Keep** the `FuzzySearch` struct, `draw_rack_view()`, `handle_rack_view()`, `list_wav_files()`, `load_sample_callback`, mouse handling, etc. – they **are not removed**.
|
||||
However, every call to engine internals (e.g., `carla_get_available_plugins`, `dispatcher_get_state`, `g_dispatch`, `carla.h` functions) **must be replaced** with a stub that does nothing (or prints a debug message) until the engine implements the corresponding FIFO commands.
|
||||
This keeps the UI code compilable and preserves the structure for future implementation.
|
||||
|
||||
## Task 2 – Implement `send_command()` and open FIFO
|
||||
- Add a function:
|
||||
|
||||
```c
|
||||
int send_command(const char *cmd_line);
|
||||
```
|
||||
|
||||
This function:
|
||||
- Opens `/tmp/looper_cmd` with `O_WRONLY`.
|
||||
- Writes the command string (e.g., `"record 0\n"`).
|
||||
- Appends a newline if missing.
|
||||
- Closes the file descriptor.
|
||||
- Returns 0 on success, -1 on error (prints diagnostic to stderr).
|
||||
|
||||
- The TUI should call `send_command()` for every user action that should affect the engine.
|
||||
|
||||
## Task 3 – Map TUI keys to FIFO commands
|
||||
Replace each `g_dispatch(action)` with a direct FIFO command string.
|
||||
|
||||
**Proposed mapping (simplified – can be refined later):**
|
||||
|
||||
| TUI key/action | FIFO command |
|
||||
|--------------------------------------------|-----------------------------------------------------|
|
||||
| `'t'` (trigger clip at selected cell) | `record <channel>\n` (channel = `selected_col`) |
|
||||
| `'d'` (reset clip) | `stop\n` (global stop) |
|
||||
| `'s'` (trigger scene – current row) | `"scene_next\n"` (or `"scene_add\n"`? TBD) |
|
||||
| `' '` (toggle transport play/pause) | No corresponding FIFO command yet. Omit for now. |
|
||||
| `'S'` (stop transport) | `"stop\n"` |
|
||||
| `'q'` (cycle quantize) | No FIFO equivalent – ignore. |
|
||||
| `'x'` (reset transport) | `"stop\n"` |
|
||||
| `'N'` (play next scene) | `"scene_next\n"` |
|
||||
| `'P'` (play previous scene) | `"scene_prev\n"` |
|
||||
| `'u'` (undo) | No FIFO equivalent – ignore. |
|
||||
| `Ctrl+R` (redo) | Ignore. |
|
||||
| `'v'`, `'V'` (visual mode) | Keep visual selection logic but send commands only for `'d'` and `'y'` actions. |
|
||||
| `'y'` (yank) | Do nothing (local clipboard only). |
|
||||
| `'p'` (paste) | For each pasted cell send `"record <ch>\n"`. |
|
||||
| `'m'` (move mode) | No effect on engine – local navigation. |
|
||||
| `'z'` (zoom grid selector) | Local navigation only. |
|
||||
| `'G'` (toggle audio/MIDI grid) | No FIFO command – ignore. |
|
||||
| `'-'` / `'='` (volume) | No FIFO command – ignore. |
|
||||
| `\t` (switch to rack view) | Remove entirely. |
|
||||
| `':'` command mode | Keep for `:q` (quit) and `:rack` commands (the latter can be removed). |
|
||||
| Escape / `'Q'` | Quit the TUI (no command sent). |
|
||||
|
||||
**Channel binding:**
|
||||
- When the user moves selection to a new column, send `"bind <col>\n"` to ensure subsequent commands affect the correct channel. This can be done in the navigation switch cases.
|
||||
|
||||
## Task 4 – Remove all references to `Engine` and `dispatcher`
|
||||
- Delete the lines:
|
||||
- `static Engine *g_engine = NULL;`
|
||||
- `static DispatchFn g_dispatch = NULL;`
|
||||
- Replace calls like `g_dispatch(action)` with `send_command(formatted_string)`.
|
||||
- Remove `dispatcher_get_state()` calls – the TUI will no longer query the engine state. Update `draw_cell()` to display only static info (clip index) or a fixed colour (e.g., all green). The state‑dependent colouring is not available without feedback from the engine. For now, show all cells as idle (white) or use a placeholder.
|
||||
- Remove the line `AppState state; dispatcher_get_state(&state);` inside draw functions.
|
||||
|
||||
## Task 5 – Simplify the `tui.h` header
|
||||
- Replace the function signatures:
|
||||
|
||||
```c
|
||||
void tui_init(void); // no Engine* argument
|
||||
void tui_run(void); // no Engine* argument
|
||||
void tui_cleanup(void);
|
||||
```
|
||||
|
||||
- Remove `#include "engine.h"` and `#include "dispatcher.h"`.
|
||||
- Remove the `Engine*` parameter from the init and run functions.
|
||||
|
||||
## Task 6 – Create `client/main.c`
|
||||
- Write a simple `main()` that:
|
||||
- Optionally opens the FIFO for writing just to check it exists.
|
||||
- Calls `tui_init()`.
|
||||
- Calls `tui_run()`.
|
||||
- Calls `tui_cleanup()`.
|
||||
- Returns 0.
|
||||
|
||||
## Task 7 – Write `client/makefile`
|
||||
- Target `looper-client`:
|
||||
- Compile `src/tui.c` and `src/main.c` (or `src/client.c` if split).
|
||||
- **Do not** link to any engine `.o` files.
|
||||
- Link only with `-lncurses` (and `-lm` if needed).
|
||||
- Example:
|
||||
```makefile
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -Wall -Wextra -g -I../engine/src
|
||||
LDFLAGS ?= -lncurses -lm
|
||||
|
||||
looper-client: src/tui.c src/main.c
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
```
|
||||
|
||||
## Task 8 – Remove dead code and unnecessary helpers
|
||||
- Delete `utils.c` / `utils.h` if they existed only for TUI – but keep all WAV‑related, rack‑related, and fuzzy‑search code (they are now stubs).
|
||||
- Remove `clip_state_to_string()`, `transport_state_to_string()`, `quantize_mode_to_string()`, `clock_source_to_string()` – they are no longer used for display because we have no `AppState`.
|
||||
Replace them with static strings that show placeholder text (e.g., `"N/A"`).
|
||||
- Remove `state_to_color()` – instead use a fixed colour pair (e.g., all cells white) or remove colour entirely, because we have no clip state.
|
||||
- Remove the `mouse` callback if it relied on `dispatcher_get_state` – but keep the function body as a no‑op.
|
||||
|
||||
## Task 9 – Test the new client
|
||||
- Build `looper-client` and verify it compiles without engine object files.
|
||||
- Start the engine (`./looper` in `engine/`).
|
||||
- Run `./looper-client` and press keys that should generate FIFO commands. Use `cat /tmp/looper_cmd` in another terminal to verify output.
|
||||
- Check that commands like `record 2`, `stop`, `bind 3` appear.
|
||||
|
||||
## Notes / Future Improvements
|
||||
- **State feedback:** The TUI currently shows clip state colours. To restore that, a separate FIFO (or shared memory) for engine‑>client status could be added. Not part of this plan.
|
||||
- **MIDI grid / rack view:** These depend on engine features not yet exposed via FIFO. They are removed; can be re‑added later.
|
||||
- **Transport commands:** The engine does not have a dedicated transport play/pause command via FIFO; it relies on MIDI notes. Future FIFO extension needed.
|
||||
|
||||
This plan produces a clean, minimal client that interfaces only through the named pipe.
|
||||
````
|
||||
BIN
client/looper-client-test
Executable file
BIN
client/looper-client-test
Executable file
Binary file not shown.
8
client/main.c
Normal file
8
client/main.c
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "tui.h"
|
||||
|
||||
int main(void) {
|
||||
tui_init();
|
||||
tui_run();
|
||||
tui_cleanup();
|
||||
return 0;
|
||||
}
|
||||
18
client/makefile
Normal file
18
client/makefile
Normal file
@@ -0,0 +1,18 @@
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -Wpedantic -std=c11
|
||||
|
||||
all: looper-client test_status_parse
|
||||
|
||||
looper-client: src/main.c src/tui.c
|
||||
$(CC) $(CFLAGS) -Isrc -o $@ $^ -lncurses
|
||||
|
||||
test_status_parse: tests/test_status_parse.c
|
||||
$(CC) $(CFLAGS) -Isrc -o test_status_parse tests/test_status_parse.c src/tui.c -lncurses
|
||||
|
||||
test: looper-client test_status_parse
|
||||
./test_status_parse
|
||||
|
||||
.PHONY: all test clean
|
||||
|
||||
clean:
|
||||
rm -f looper-client test_status_parse
|
||||
8
client/src/main.c
Normal file
8
client/src/main.c
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "tui.h"
|
||||
|
||||
int main(void) {
|
||||
tui_init();
|
||||
tui_run();
|
||||
tui_cleanup();
|
||||
return 0;
|
||||
}
|
||||
238
client/src/tui.c
Normal file
238
client/src/tui.c
Normal file
@@ -0,0 +1,238 @@
|
||||
#include "tui.h"
|
||||
#include <ncurses.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <math.h>
|
||||
|
||||
/* ---------- FIFO command helper ---------- */
|
||||
int send_command(const char *cmd) {
|
||||
const char *fifo_path = getenv("LOOPER_CMD_FIFO");
|
||||
if (!fifo_path) fifo_path = "/tmp/looper_cmd";
|
||||
int fd = open(fifo_path, O_WRONLY | O_NONBLOCK);
|
||||
if (fd < 0) return -1;
|
||||
size_t len = strlen(cmd);
|
||||
int n = write(fd, cmd, len);
|
||||
if (n == (int)len && cmd[len-1] != '\n')
|
||||
write(fd, "\n", 1);
|
||||
close(fd);
|
||||
return (n >= 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ---------- Stub functions (no engine) ---------- */
|
||||
// Clip states – dummy values used as placeholders
|
||||
typedef enum { CLIP_EMPTY, CLIP_RECORDING, CLIP_LOOPING, CLIP_STOPPED } ClipState;
|
||||
static const char *clip_state_string(ClipState s) { (void)s; return "?"; }
|
||||
|
||||
/* Grid dimensions */
|
||||
#define GRID_ROWS 8
|
||||
#define GRID_COLS 8
|
||||
#define NUM_GRIDS 8
|
||||
#define CELL_WIDTH 6
|
||||
#define CELL_HEIGHT 3
|
||||
|
||||
/* status FIFO path */
|
||||
#define STATUS_FIFO "/tmp/looper_status"
|
||||
#define CMD_FIFO "/tmp/looper_cmd"
|
||||
|
||||
/* Per‑cell state array (indexed by row*GRID_COLS+col) */
|
||||
typedef enum { STATE_IDLE, STATE_RECORD, STATE_LOOPING, STATE_PAUSED } ChannelState;
|
||||
static ChannelState cell_state[GRID_ROWS * GRID_COLS];
|
||||
|
||||
/* Color pairs */
|
||||
enum {
|
||||
COLOR_EMPTY=1, COLOR_RECORDING, COLOR_LOOPING, COLOR_STOPPED,
|
||||
COLOR_SELECTED, COLOR_HELP
|
||||
};
|
||||
|
||||
static int selected_row = 0, selected_col = 0;
|
||||
static int selected_grid = 0;
|
||||
static bool show_help = false;
|
||||
|
||||
/* Visual mode, marks, yank buffer – keep but only local state */
|
||||
static int marks[26];
|
||||
typedef struct { int *clip_indices; int count; } YankBuffer;
|
||||
static YankBuffer yank_buffer = {NULL, 0};
|
||||
typedef enum { MODE_NORMAL, MODE_VISUAL, MODE_MOVE } UIMode;
|
||||
static UIMode current_mode = MODE_NORMAL;
|
||||
static int visual_start_row, visual_start_col, visual_end_row, visual_end_col;
|
||||
|
||||
/* Fuzzy search – keep struct but stub carla calls */
|
||||
typedef struct {
|
||||
char query[256]; int query_len, selected_index, num_results;
|
||||
int result_indices[256]; bool active; char prompt[64];
|
||||
void (*callback)(const char *);
|
||||
const char **items; int num_items; bool free_items;
|
||||
} FuzzySearch;
|
||||
static FuzzySearch fuzzy_search = {0};
|
||||
|
||||
/* ---------- Parse status line from engine status FIFO ---------- */
|
||||
bool parse_status_line(const char *line, int *ch, int *scene, ChannelState *state) {
|
||||
int sta;
|
||||
if (sscanf(line, "CH=%d SC=%d STATE=%d", ch, scene, &sta) == 3) {
|
||||
if (sta >= 0 && sta <= 3) {
|
||||
*state = (ChannelState)sta;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/* try text-based format */
|
||||
char state_str[32];
|
||||
if (sscanf(line, "CH=%d SC=%d STATE=%31s", ch, scene, state_str) != 3)
|
||||
return false;
|
||||
if (strcmp(state_str, "IDLE") == 0) { *state = STATE_IDLE; return true; }
|
||||
if (strcmp(state_str, "RECORD") == 0) { *state = STATE_RECORD; return true; }
|
||||
if (strcmp(state_str, "LOOPING") == 0) { *state = STATE_LOOPING; return true; }
|
||||
if (strcmp(state_str, "PAUSED") == 0) { *state = STATE_PAUSED; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---------- State to color (uses cell_state array) ---------- */
|
||||
static int state_to_color(ChannelState s) {
|
||||
switch (s) {
|
||||
case STATE_IDLE: return COLOR_EMPTY;
|
||||
case STATE_RECORD: return COLOR_RECORDING;
|
||||
case STATE_LOOPING: return COLOR_LOOPING;
|
||||
case STATE_PAUSED: return COLOR_STOPPED;
|
||||
default: return COLOR_EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Draw cell (no AppState) ---------- */
|
||||
static void draw_cell(int grid, int row, int col, bool selected) {
|
||||
int y = row * CELL_HEIGHT + 3;
|
||||
int x = col * CELL_WIDTH + 1;
|
||||
int idx = row * GRID_COLS + col;
|
||||
ChannelState s = cell_state[idx];
|
||||
int color = selected ? COLOR_SELECTED : state_to_color(s);
|
||||
attron(COLOR_PAIR(color));
|
||||
for (int dy=0; dy<CELL_HEIGHT; dy++)
|
||||
for (int dx=0; dx<CELL_WIDTH; dx++)
|
||||
mvaddch(y+dy, x+dx, ' ');
|
||||
mvprintw(y+1, x+1, "%2d", grid*GRID_ROWS*GRID_COLS + row*GRID_COLS + col);
|
||||
attroff(COLOR_PAIR(color));
|
||||
}
|
||||
|
||||
static void draw_grid(void) {
|
||||
clear();
|
||||
attron(A_BOLD);
|
||||
mvprintw(0,0,"JACK Looper - Client (FIFO only)");
|
||||
attroff(A_BOLD);
|
||||
for (int r=0; r<GRID_ROWS; r++)
|
||||
for (int c=0; c<GRID_COLS; c++)
|
||||
draw_cell(selected_grid, r, c, r==selected_row && c==selected_col);
|
||||
mvprintw(GRID_ROWS*CELL_HEIGHT+3, 0, "Selected: Grid %d, Row %d, Col %d",
|
||||
selected_grid, selected_row, selected_col);
|
||||
if (show_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");
|
||||
attroff(COLOR_PAIR(COLOR_HELP));
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
/* ---------- TUI init ---------- */
|
||||
void tui_init(void) {
|
||||
initscr();
|
||||
cbreak(); noecho(); keypad(stdscr, TRUE); curs_set(0);
|
||||
if (!has_colors()) { endwin(); fprintf(stderr,"No colors\n"); exit(1); }
|
||||
start_color();
|
||||
init_pair(COLOR_EMPTY, COLOR_WHITE, COLOR_BLACK);
|
||||
init_pair(COLOR_RECORDING, COLOR_RED, COLOR_BLACK);
|
||||
init_pair(COLOR_LOOPING, COLOR_GREEN, COLOR_BLACK);
|
||||
init_pair(COLOR_STOPPED, COLOR_BLUE, COLOR_BLACK);
|
||||
init_pair(COLOR_SELECTED, COLOR_BLACK, COLOR_CYAN);
|
||||
init_pair(COLOR_HELP, COLOR_CYAN, COLOR_BLACK);
|
||||
for (int i=0;i<26;i++) marks[i] = -1;
|
||||
/* initialise cell states to idle */
|
||||
for (int i = 0; i < GRID_ROWS * GRID_COLS; i++)
|
||||
cell_state[i] = STATE_IDLE;
|
||||
}
|
||||
|
||||
/* ---------- TUI run ---------- */
|
||||
void tui_run(void) {
|
||||
draw_grid();
|
||||
while (1) {
|
||||
/* read any available status lines */
|
||||
int fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
|
||||
if (fd >= 0) {
|
||||
char buf[256];
|
||||
int n = read(fd, buf, sizeof(buf)-1);
|
||||
if (n > 0) {
|
||||
buf[n] = '\0';
|
||||
char *line = buf;
|
||||
while (*line) {
|
||||
char *nl = strchr(line, '\n');
|
||||
if (nl) *nl = '\0';
|
||||
int ch, sc;
|
||||
ChannelState st;
|
||||
if (parse_status_line(line, &ch, &sc, &st)) {
|
||||
if (ch >= 0 && ch < GRID_ROWS * GRID_COLS)
|
||||
cell_state[ch] = st;
|
||||
}
|
||||
if (nl) {
|
||||
*nl = '\n';
|
||||
line = nl + 1;
|
||||
} else break;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
int chc = getch();
|
||||
switch (chc) {
|
||||
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 'k': case KEY_UP: selected_row = (selected_row-1+GRID_ROWS)%GRID_ROWS; break;
|
||||
case 'l': case KEY_RIGHT: selected_col = (selected_col+1)%GRID_COLS; break;
|
||||
case 't': {
|
||||
char cmd[32];
|
||||
snprintf(cmd, sizeof(cmd), "record %d\n", selected_col);
|
||||
send_command(cmd);
|
||||
break;
|
||||
}
|
||||
case 's':
|
||||
send_command("scene_next\n");
|
||||
break;
|
||||
case 'S':
|
||||
send_command("scene_prev\n");
|
||||
break;
|
||||
case 'd': case 'D':
|
||||
send_command("stop\n");
|
||||
break;
|
||||
case 'a':
|
||||
send_command("add\n");
|
||||
break;
|
||||
case 'A':
|
||||
send_command("add_midi\n");
|
||||
break;
|
||||
case 'r':
|
||||
send_command("remove\n");
|
||||
break;
|
||||
case 'b': {
|
||||
char cmd[16];
|
||||
snprintf(cmd, sizeof(cmd), "bind %d\n", selected_col);
|
||||
send_command(cmd);
|
||||
break;
|
||||
}
|
||||
case 'u':
|
||||
send_command("unbind\n");
|
||||
break;
|
||||
case '?': show_help = !show_help; break;
|
||||
case 27: case 'Q': return;
|
||||
default: break;
|
||||
}
|
||||
draw_grid();
|
||||
}
|
||||
}
|
||||
|
||||
void tui_cleanup(void) {
|
||||
if (yank_buffer.clip_indices) free(yank_buffer.clip_indices);
|
||||
/* delete FIFOs */
|
||||
unlink(STATUS_FIFO);
|
||||
unlink(CMD_FIFO);
|
||||
curs_set(1); endwin();
|
||||
}
|
||||
9
client/src/tui.h
Normal file
9
client/src/tui.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifndef TUI_H
|
||||
#define TUI_H
|
||||
|
||||
void tui_init(void);
|
||||
void tui_run(void);
|
||||
void tui_cleanup(void);
|
||||
int send_command(const char *cmd);
|
||||
|
||||
#endif
|
||||
67
client/tests/test_client.c
Normal file
67
client/tests/test_client.c
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "tui.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define TEST_PASS 0
|
||||
#define TEST_FAIL 1
|
||||
|
||||
static int run_single_test(const char *test_name, const char *cmd_sent, const char *expected) {
|
||||
/* build temporary file path */
|
||||
char tmpl[] = "/tmp/looper_test_XXXXXX";
|
||||
int fd = mkstemp(tmpl);
|
||||
if (fd == -1) { perror("mkstemp"); return TEST_FAIL; }
|
||||
close(fd);
|
||||
/* create regular file to mimic a FIFO */
|
||||
fd = open(tmpl, O_CREAT|O_WRONLY|O_TRUNC, 0644);
|
||||
if (fd < 0) { perror("open create"); unlink(tmpl); return TEST_FAIL; }
|
||||
close(fd);
|
||||
|
||||
/* make send_command use this file */
|
||||
setenv("LOOPER_CMD_FIFO", tmpl, 1);
|
||||
|
||||
int ret = send_command(cmd_sent);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "FAIL %s: send_command returned %d\n", test_name, ret);
|
||||
unlink(tmpl);
|
||||
return TEST_FAIL;
|
||||
}
|
||||
|
||||
/* read back the written content */
|
||||
FILE *fp = fopen(tmpl, "r");
|
||||
if (!fp) { perror("fopen"); unlink(tmpl); return TEST_FAIL; }
|
||||
char buf[4096];
|
||||
size_t nread = fread(buf, 1, sizeof(buf)-1, fp);
|
||||
fclose(fp);
|
||||
buf[nread] = '\0';
|
||||
|
||||
/* build expected string (send_command always appends a newline) */
|
||||
char expected_line[512];
|
||||
snprintf(expected_line, sizeof(expected_line), "%s\n", expected);
|
||||
|
||||
if (strcmp(buf, expected_line) == 0) {
|
||||
printf("PASS %s\n", test_name);
|
||||
unlink(tmpl);
|
||||
return TEST_PASS;
|
||||
} else {
|
||||
printf("FAIL %s: expected '%s', got '%s'\n", test_name, expected_line, buf);
|
||||
unlink(tmpl);
|
||||
return TEST_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
int fail = 0;
|
||||
fail += run_single_test("record_0", "record 0", "record 0");
|
||||
fail += run_single_test("record_1", "record 1", "record 1");
|
||||
fail += run_single_test("stop", "stop", "stop");
|
||||
fail += run_single_test("scene_next", "scene_next", "scene_next");
|
||||
fail += run_single_test("scene_prev", "scene_prev", "scene_prev");
|
||||
fail += run_single_test("bind_2", "bind 2", "bind 2");
|
||||
fail += run_single_test("with_newline", "record 0\n", "record 0");
|
||||
printf("%d tests failed.\n", fail);
|
||||
return fail > 0 ? 1 : 0;
|
||||
}
|
||||
88
client/tests/test_status_parse.c
Normal file
88
client/tests/test_status_parse.c
Normal file
@@ -0,0 +1,88 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum { STATE_IDLE, STATE_RECORD, STATE_LOOPING, STATE_PAUSED } ChannelState;
|
||||
|
||||
bool parse_status_line(const char *line, int *ch, int *scene, ChannelState *state);
|
||||
|
||||
static int test_parse_idle(void) {
|
||||
printf("Test parse_status_line: IDLE\n");
|
||||
int ch, sc; ChannelState st;
|
||||
if (!parse_status_line("CH=0 SC=0 STATE=IDLE\n", &ch, &sc, &st)) {
|
||||
fprintf(stderr, " FAIL: parse returned false\n");
|
||||
return 1;
|
||||
}
|
||||
if (ch != 0 || sc != 0 || st != STATE_IDLE) {
|
||||
fprintf(stderr, " FAIL: expected (0,0,IDLE), got (%d,%d,%d)\n", ch, sc, st);
|
||||
return 1;
|
||||
}
|
||||
printf(" PASS\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_parse_recording(void) {
|
||||
printf("Test parse_status_line: RECORD\n");
|
||||
int ch, sc; ChannelState st;
|
||||
if (!parse_status_line("CH=0 SC=0 STATE=RECORD\n", &ch, &sc, &st)) {
|
||||
fprintf(stderr, " FAIL: parse returned false\n");
|
||||
return 1;
|
||||
}
|
||||
if (ch != 0 || sc != 0 || st != STATE_RECORD) {
|
||||
fprintf(stderr, " FAIL: expected (0,0,RECORD), got (%d,%d,%d)\n", ch, sc, st);
|
||||
return 1;
|
||||
}
|
||||
printf(" PASS\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_parse_looping(void) {
|
||||
printf("Test parse_status_line: LOOPING\n");
|
||||
int ch, sc; ChannelState st;
|
||||
if (!parse_status_line("CH=0 SC=0 STATE=LOOPING\n", &ch, &sc, &st)) {
|
||||
fprintf(stderr, " FAIL: parse returned false\n");
|
||||
return 1;
|
||||
}
|
||||
if (ch != 0 || sc != 0 || st != STATE_LOOPING) {
|
||||
fprintf(stderr, " FAIL: expected (0,0,LOOPING), got (%d,%d,%d)\n", ch, sc, st);
|
||||
return 1;
|
||||
}
|
||||
printf(" PASS\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_parse_paused(void) {
|
||||
printf("Test parse_status_line: PAUSED\n");
|
||||
int ch, sc; ChannelState st;
|
||||
if (!parse_status_line("CH=0 SC=0 STATE=PAUSED\n", &ch, &sc, &st)) {
|
||||
fprintf(stderr, " FAIL: parse returned false\n");
|
||||
return 1;
|
||||
}
|
||||
if (ch != 0 || sc != 0 || st != STATE_PAUSED) {
|
||||
fprintf(stderr, " FAIL: expected (0,0,PAUSED), got (%d,%d,%d)\n", ch, sc, st);
|
||||
return 1;
|
||||
}
|
||||
printf(" PASS\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_parse_malformed(void) {
|
||||
printf("Test parse_status_line: malformed\n");
|
||||
int ch, sc; ChannelState st;
|
||||
if (parse_status_line("garbage\n", &ch, &sc, &st)) {
|
||||
fprintf(stderr, " FAIL: parse should return false for garbage\n");
|
||||
return 1;
|
||||
}
|
||||
printf(" PASS\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
int fail = 0;
|
||||
fail += test_parse_idle();
|
||||
fail += test_parse_recording();
|
||||
fail += test_parse_looping();
|
||||
fail += test_parse_paused();
|
||||
fail += test_parse_malformed();
|
||||
return fail;
|
||||
}
|
||||
Reference in New Issue
Block a user