#include "../../src/gui/main_window.hpp" #include #include #include #include #include #include #include #include #include #include /* 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(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(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(window.child(i))) menu = found; if (auto *found = dynamic_cast(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; }