1
0
Fork 0
forked from hanez/xtc

Connect FLTK shell to JACK engine controls and status

This commit is contained in:
Johannes Findeisen 2026-10-26 23:40:28 +01:00
commit 2ecc03b482
14 changed files with 691 additions and 45 deletions

View file

@ -0,0 +1,204 @@
#include "../../src/gui/main_window.hpp"
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Group.H>
#include <FL/Fl_Menu_Bar.H>
#include <FL/Fl_Tabs.H>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <initializer_list>
/* Fake only the public C backend boundary. Real FLTK widgets, callbacks and
* timers run under Xvfb; no server/device is required or touched. */
struct xtc_backend { int running = 0; };
namespace {
xtc_backend handle;
xtc_result open_result = XTC_OK, start_result = XTC_OK, stop_result = XTC_OK, close_result = XTC_OK;
xtc_result status_result = XTC_OK;
xtc_backend_status engine_status = {};
bool partial_open = false;
unsigned int opens = 0, starts = 0, stops = 0, closes = 0, collects = 0, alive = 0;
int failures = 0;
#define CHECK(c) do { if (!(c)) { std::fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); ++failures; } } while (0)
void pump()
{
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(180);
while (std::chrono::steady_clock::now() < end) Fl::wait(0.02);
}
Fl_Widget *find(Fl_Group *group, const char *label)
{
for (int i = 0; i < group->children(); ++i) {
Fl_Widget *child = group->child(i);
if (child->label() != nullptr && std::strcmp(child->label(), label) == 0) return child;
auto *nested = dynamic_cast<Fl_Group *>(child);
if (nested != nullptr) if (auto *match = find(nested, label)) return match;
}
return nullptr;
}
bool contains(Fl_Group *group, const char *text)
{
for (int i = 0; i < group->children(); ++i) {
Fl_Widget *child = group->child(i);
if (child->label() != nullptr && std::strstr(child->label(), text) != nullptr) return true;
if (auto *nested = dynamic_cast<Fl_Group *>(child)) if (contains(nested, text)) return true;
}
return false;
}
void press(Fl_Widget *widget)
{
CHECK(widget != nullptr);
if (widget != nullptr) widget->do_callback();
}
void reset()
{
CHECK(alive == 0U);
open_result = start_result = stop_result = close_result = status_result = XTC_OK;
engine_status = {};
engine_status.sample_rate = 48000U;
engine_status.buffer_frames = 1024U;
partial_open = false;
}
}
extern "C" {
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out)
{
CHECK(config->kind == XTC_BACKEND_JACK && config->channel_count == 1U);
++opens;
*out = open_result == XTC_OK || partial_open ? &handle : nullptr;
if (*out != nullptr) { ++alive; handle.running = 0; }
return open_result;
}
xtc_result xtc_backend_close(xtc_backend *backend)
{
if (backend == nullptr) return XTC_OK;
++closes;
if (close_result == XTC_OK) { CHECK(alive == 1U); --alive; handle.running = 0; }
return close_result;
}
xtc_result xtc_backend_start(xtc_backend *backend)
{ CHECK(backend == &handle); ++starts; if (start_result == XTC_OK) handle.running = 1; return start_result; }
xtc_result xtc_backend_stop(xtc_backend *backend)
{ CHECK(backend == &handle); ++stops; if (stop_result == XTC_OK) handle.running = 0; return stop_result; }
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *status)
{
CHECK(backend == &handle && alive == 1U);
*status = engine_status;
status->running = handle.running && !status->server_lost;
return status_result;
}
void xtc_backend_collect(xtc_backend *backend)
{ CHECK(backend == &handle && alive == 1U); ++collects; }
}
int main(int argc, char **argv)
{
reset();
{
MainWindow window;
window.show(argc, argv);
auto *play = find(&window, "Play");
auto *stop = find(&window, "Stop");
auto *reconnect = find(&window, "Reconnect");
CHECK(play != nullptr && stop != nullptr && reconnect != nullptr);
if (play == nullptr || stop == nullptr || reconnect == nullptr) return 1;
CHECK(play->active() && !stop->active());
CHECK(contains(&window, "ready / stopped"));
press(play);
CHECK(starts == 1U && !play->active() && stop->active());
CHECK(contains(&window, "running (silence"));
press(stop);
CHECK(stops == 1U && play->active() && !stop->active());
start_result = XTC_ERR_IO;
press(play);
CHECK(contains(&window, "Start engine failed: I/O error"));
pump(); // A refresh must not erase a command error.
CHECK(contains(&window, "Start engine failed: I/O error"));
start_result = XTC_OK;
press(play);
CHECK(!contains(&window, "Start engine failed"));
stop_result = XTC_ERR_IO;
press(stop);
CHECK(contains(&window, "Stop engine failed") && stop->active());
stop_result = XTC_OK;
press(stop);
Fl_Menu_Bar *menu = nullptr;
Fl_Tabs *tabs = nullptr;
for (int i = 0; i < window.children(); ++i) {
if (auto *found = dynamic_cast<Fl_Menu_Bar *>(window.child(i))) menu = found;
if (auto *found = dynamic_cast<Fl_Tabs *>(window.child(i))) tabs = found;
}
CHECK(menu != nullptr && tabs != nullptr);
if (menu == nullptr || tabs == nullptr) return 1;
for (const char *view : {"View/Mixer", "View/Modular Graph", "View/Sequencer"}) {
const Fl_Menu_Item *item = menu->find_item(view);
CHECK(item != nullptr);
if (item != nullptr) {
menu->picked(item);
CHECK(std::strcmp(tabs->value()->label(), std::strchr(view, '/') + 1) == 0);
}
}
engine_status.xruns = 3U;
engine_status.processed_blocks = 123U;
pump();
CHECK(contains(&window, "123 blocks | 3 xruns"));
engine_status.sample_rate_changed = 1;
pump();
CHECK(contains(&window, "sample rate changed") && !play->active());
engine_status.sample_rate_changed = 0;
engine_status.server_lost = 1;
pump();
CHECK(contains(&window, "server lost") && !play->active());
engine_status.server_lost = 0;
press(reconnect);
CHECK(contains(&window, "ready / stopped") && play->active());
status_result = XTC_ERR_IO;
pump();
CHECK(contains(&window, "status unavailable") && !play->active());
status_result = XTC_OK;
press(reconnect);
close_result = XTC_ERR_IO;
press(reconnect);
CHECK(contains(&window, "Reconnect could not close JACK") && alive == 1U);
window.do_callback();
CHECK(window.shown() && alive == 1U && contains(&window, "window kept open for retry"));
close_result = XTC_OK;
menu->picked(menu->find_item("File/Quit"));
CHECK(!window.shown() && alive == 0U);
unsigned int before = collects;
pump();
CHECK(collects == before); // Removed timer cannot call a closed backend.
}
for (xtc_result failure : {XTC_ERR_IO, XTC_ERR_UNSUPPORTED}) {
reset();
open_result = failure;
MainWindow window;
window.show(argc, argv);
CHECK(contains(&window, "Open JACK failed") && !find(&window, "Play")->active());
open_result = XTC_OK;
press(find(&window, "Reconnect"));
CHECK(find(&window, "Play")->active());
press(find(&window, "Play"));
window.do_callback(); // Close while running, without a preceding stop.
CHECK(!window.shown() && alive == 0U);
}
reset();
partial_open = true;
open_result = XTC_ERR_IO;
{
MainWindow window;
CHECK(alive == 1U && !find(&window, "Play")->active());
window.do_callback(); // Partially initialized handles are closed only.
CHECK(alive == 0U);
}
reset();
for (int i = 0; i < 10; ++i) {
MainWindow window;
press(find(&window, "Play"));
window.do_callback();
}
CHECK(alive == 0U);
if (failures != 0) return 1;
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
return 0;
}