Connect FLTK shell to JACK engine controls and status
This commit is contained in:
parent
577d80f018
commit
2ecc03b482
14 changed files with 691 additions and 45 deletions
|
|
@ -26,3 +26,40 @@ format change is performed on the shared server. Those paths, missing server,
|
|||
port/callback setup failure, activation/deactivation/close failure, null buffers,
|
||||
invalid configuration, oversize buffers, and reopen are tested with injected
|
||||
JACK callbacks/results in `tests/unit/test_backend.c`.
|
||||
|
||||
## FLTK shell tests
|
||||
|
||||
```sh
|
||||
make gui test-gui
|
||||
# Optional, requires a running JACK-compatible server:
|
||||
make test-gui-jack
|
||||
```
|
||||
|
||||
These targets require FLTK development files, `xvfb-run` and Xvfb. No additional
|
||||
GUI test library is needed. `test_gui.cpp` uses real widgets, menus and timers
|
||||
with a fake public C backend. It covers Play/Stop/Reconnect, view switching,
|
||||
persistent startup/control errors, server loss/rate changes, partial handles,
|
||||
close-failure retry and timer removal. All 18 opened handles are closed.
|
||||
`test_gui_jack.cpp` uses the real backend, repeats start/stop nine times across
|
||||
three windows, reconnects, and closes each window while running. No audio ports
|
||||
are connected. Both tests passed on the inspected host.
|
||||
|
||||
A separate xdotool smoke run against `build/xtc` under a 1280x800 Xvfb display
|
||||
exercised F5/F8/F6, Space, Ctrl+1/2/3 and Ctrl+Q. The app exited successfully;
|
||||
visual inspection confirmed running status and mixer view switching.
|
||||
|
||||
Clang ASan/UBSan found no application errors in the GUI tests. The unsuppressed
|
||||
run reports exit-time allocations originating in the installed fontconfig
|
||||
library used by FLTK/Pango. `lsan.supp` narrowly suppresses that library; leak
|
||||
checking stays enabled for application allocations. This is not a claim that
|
||||
the third-party font stack is leak-free. Apply only to GUI sanitizer tests:
|
||||
|
||||
```sh
|
||||
LSAN_OPTIONS=suppressions=tests/integration/lsan.supp xvfb-run -a -s '-screen 0 1280x800x24' ./build/gui-sanitize/test_gui
|
||||
```
|
||||
|
||||
Build that binary with Clang/Clang++, `-fsanitize=address,undefined` in both
|
||||
CFLAGS/CXXFLAGS and LDFLAGS, using the baseline warning/include/standard flags
|
||||
from BUILD.md. Keep FLTK's `-I` and `-D` flags from `fltk-config --cxxflags`,
|
||||
but exclude the distribution's GCC-specific LTO/object-code flags for Clang.
|
||||
No suppressions are used for core/backend/realtime sanitizer tests.
|
||||
|
|
|
|||
4
tests/integration/lsan.supp
Normal file
4
tests/integration/lsan.supp
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Installed FLTK/Pango/fontconfig retain font caches at process exit.
|
||||
# Keep leak detection active for application allocations and backend handles.
|
||||
# The unsuppressed GUI run reports allocation stacks in libfontconfig.so.1.
|
||||
leak:libfontconfig.so
|
||||
204
tests/integration/test_gui.cpp
Normal file
204
tests/integration/test_gui.cpp
Normal 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;
|
||||
}
|
||||
60
tests/integration/test_gui_jack.cpp
Normal file
60
tests/integration/test_gui_jack.cpp
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#include "../../src/gui/main_window.hpp"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Group.H>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
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::strstr(child->label(), label) != nullptr) return child;
|
||||
if (auto *nested = dynamic_cast<Fl_Group *>(child)) if (auto *match = find(*nested, label)) return match;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
void pump()
|
||||
{
|
||||
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(400);
|
||||
while (std::chrono::steady_clock::now() < end) Fl::wait(0.02);
|
||||
}
|
||||
}
|
||||
#define CHECK(c) do { if (!(c)) { std::fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); return 1; } } while (0)
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Real JACK backend and real FLTK event loop. No routes, no playback data.
|
||||
for (int iteration = 0; iteration < 3; ++iteration) {
|
||||
MainWindow window;
|
||||
window.show(argc, argv);
|
||||
CHECK(find(window, "ready / stopped") != nullptr);
|
||||
auto *play = find(window, "Play");
|
||||
auto *stop = find(window, "Stop");
|
||||
auto *reconnect = find(window, "Reconnect");
|
||||
CHECK(play != nullptr && stop != nullptr && reconnect != nullptr);
|
||||
for (int restart = 0; restart < 3; ++restart) {
|
||||
CHECK(play->active());
|
||||
play->do_callback();
|
||||
pump();
|
||||
CHECK(find(window, "running (silence") != nullptr);
|
||||
auto *status = find(window, "JACK |");
|
||||
CHECK(status != nullptr);
|
||||
unsigned int rate = 0U, frames = 0U, blocks = 0U;
|
||||
CHECK(std::sscanf(status->label(), "JACK | %u Hz | %u frames | mono | %u blocks", &rate, &frames, &blocks) == 3);
|
||||
CHECK(rate > 0U && frames > 0U && blocks > 0U);
|
||||
stop->do_callback();
|
||||
CHECK(find(window, "ready / stopped") != nullptr);
|
||||
}
|
||||
reconnect->do_callback();
|
||||
CHECK(find(window, "ready / stopped") != nullptr);
|
||||
play->do_callback();
|
||||
pump();
|
||||
CHECK(find(window, "running (silence") != nullptr);
|
||||
window.do_callback(); // Close with an active client; synchronizes callbacks.
|
||||
CHECK(!window.shown());
|
||||
pump(); // No timer may access the closed client.
|
||||
}
|
||||
std::puts("Live GUI/JACK start/stop/reconnect/active-close tests passed.");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue