Revert "Added an alternative GUI based on JUCE; I like FLTK more..."
This reverts commit cd2f87a8b3.
This commit is contained in:
parent
cd2f87a8b3
commit
d6e46f67d7
58 changed files with 10 additions and 11136 deletions
|
|
@ -254,29 +254,3 @@ It requires an existing JACK/PipeWire server, changes no user preferences and
|
|||
connects no physical outputs. Enable `XTC_ENABLE_LIVE_TESTS` to include
|
||||
`playback` in CTest. The fake-backend `gui-midi-workflows` test also checks absent
|
||||
input/output/both, returning devices and genuine backend failures.
|
||||
|
||||
The optional JUCE workstation builds the shared GUI regression source against
|
||||
`src/gui/JUCE` as `test_juce_gui`; `juce-gui-*` CTests cover the same retained
|
||||
commands, fault injection, dialogs, hierarchy and gestures without FLTK. Build
|
||||
all test fixtures before running them. The real-backend `test_juce_playback`
|
||||
delivers JUCE MouseEvents to native transport buttons, resumes with a JUCE
|
||||
KeyPress, and checks finite nonzero stereo samples through a private JACK probe
|
||||
with missing saved MIDI devices. It writes no user preferences and connects no
|
||||
physical ports. Run it from the repository root under Xvfb after building:
|
||||
|
||||
```sh
|
||||
cmake --build build/juce-gui --parallel 4
|
||||
ctest --test-dir build/juce-gui -R juce-gui --output-on-failure -j1
|
||||
xvfb-run -a ./build/juce-gui/test_juce_playback
|
||||
```
|
||||
|
||||
`XTC_ENABLE_LIVE_TESTS` registers the latter as `juce-playback`. An optional
|
||||
`XTC_JUCE_SCREENSHOT` absolute output path captures the actual workstation
|
||||
component during that live regression.
|
||||
|
||||
Linux qualification used JUCE 8.0.15: all 12 JUCE GUI workflows passed (with
|
||||
focused reruns after native focus/blur fixes), alongside all 57 existing
|
||||
FLTK/core tests. Native Play/Stop/Space, expanded-track rendering and finite
|
||||
stereo output passed with deliberately unavailable saved MIDI devices. Bare
|
||||
Xvfb can emit JUCE X11 BadAtom diagnostics without a window manager; these do
|
||||
not indicate a failed assertion in the workflow checks.
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
/* Native JUCE GUI playback regression. Connects only to a private software probe;
|
||||
* neither user preferences nor physical audio/MIDI routes are changed. */
|
||||
#include "../../src/gui/JUCE/main_window.hpp"
|
||||
#include "../../src/gui/JUCE/sequencer_track_widget.hpp"
|
||||
#include "../../src/gui/JUCE/sequencer_widget.hpp"
|
||||
#include "../../src/gui/JUCE/ui.hpp"
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <jack/jack.h>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
|
||||
#define REQUIRE(c) \
|
||||
do \
|
||||
{ \
|
||||
if (!(c)) \
|
||||
throw std::runtime_error(#c); \
|
||||
} while (0)
|
||||
namespace
|
||||
{
|
||||
struct Probe
|
||||
{
|
||||
jack_client_t *client = nullptr;
|
||||
jack_port_t *ports[2]{};
|
||||
std::atomic<bool> heard[2]{};
|
||||
std::atomic<bool> invalid{false};
|
||||
~Probe()
|
||||
{
|
||||
if (client)
|
||||
jack_client_close(client);
|
||||
}
|
||||
static int process(jack_nframes_t frames, void *data)
|
||||
{
|
||||
auto &probe = *static_cast<Probe *>(data);
|
||||
for (int channel = 0; channel < 2; ++channel)
|
||||
{
|
||||
auto *samples =
|
||||
static_cast<const float *>(jack_port_get_buffer(probe.ports[channel], frames));
|
||||
for (jack_nframes_t i = 0; i < frames; ++i)
|
||||
{
|
||||
if (!std::isfinite(samples[i]))
|
||||
probe.invalid.store(true, std::memory_order_relaxed);
|
||||
if (std::fabs(samples[i]) > .001f)
|
||||
probe.heard[channel].store(true, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
struct TemporaryDirectory
|
||||
{
|
||||
std::string path;
|
||||
TemporaryDirectory()
|
||||
{
|
||||
char name[] = "/tmp/xtc-playback-XXXXXX";
|
||||
REQUIRE(mkdtemp(name));
|
||||
path = name;
|
||||
}
|
||||
~TemporaryDirectory() { std::filesystem::remove_all(path); }
|
||||
};
|
||||
UiWidget *find(UiGroup &group, const char *text, bool exact = true)
|
||||
{
|
||||
for (int i = 0; i < group.children(); ++i)
|
||||
{
|
||||
auto *child = group.child(i);
|
||||
if (child->label() && (exact ? !std::strcmp(child->label(), text)
|
||||
: std::strstr(child->label(), text) != nullptr))
|
||||
return child;
|
||||
if (auto *nested = dynamic_cast<UiGroup *>(child))
|
||||
if (auto *result = find(*nested, text, exact))
|
||||
return result;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
SequencerWidget *timeline(UiGroup &group)
|
||||
{
|
||||
for (int i = 0; i < group.children(); ++i)
|
||||
{
|
||||
if (auto *s = dynamic_cast<SequencerWidget *>(group.child(i)))
|
||||
return s;
|
||||
if (auto *nested = dynamic_cast<UiGroup *>(group.child(i)))
|
||||
if (auto *s = timeline(*nested))
|
||||
return s;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
void pump(int milliseconds)
|
||||
{
|
||||
auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds);
|
||||
while (std::chrono::steady_clock::now() < end)
|
||||
Ui::wait(.01);
|
||||
}
|
||||
void mouse_click(juce::Component *target, juce::Point<float> point)
|
||||
{
|
||||
REQUIRE(target);
|
||||
auto now = juce::Time::getCurrentTime();
|
||||
juce::MouseEvent down(juce::Desktop::getInstance().getMainMouseSource(), point,
|
||||
juce::ModifierKeys(juce::ModifierKeys::leftButtonModifier), 1, 0, 0, 0, 0,
|
||||
target, target, now, point, now, 1, false);
|
||||
target->mouseDown(down);
|
||||
juce::MouseEvent up(juce::Desktop::getInstance().getMainMouseSource(), point,
|
||||
juce::ModifierKeys(), 1, 0, 0, 0, 0, target, target, now, point, now, 1,
|
||||
false);
|
||||
target->mouseUp(up);
|
||||
}
|
||||
void click(MainWindow &, UiWidget *button)
|
||||
{
|
||||
REQUIRE(button && button->active());
|
||||
// Deliver JUCE MouseEvents to the native TextButton child.
|
||||
juce::Component *target = nullptr;
|
||||
for (int i = 0; i < button->getNumChildComponents(); ++i)
|
||||
if (dynamic_cast<juce::Button *>(button->getChildComponent(i)))
|
||||
target = button->getChildComponent(i);
|
||||
REQUIRE(target);
|
||||
mouse_click(target, target->getLocalBounds().toFloat().getCentre());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
int main()
|
||||
{
|
||||
UiInitialiser initialise;
|
||||
try
|
||||
{
|
||||
TemporaryDirectory directory;
|
||||
xtc_settings settings{};
|
||||
xtc_settings_defaults(&settings);
|
||||
std::strcpy(settings.midi_input_port, "xtc-missing-production-keyboard:out");
|
||||
std::strcpy(settings.midi_output_port, "xtc-missing-production-synth:in");
|
||||
std::snprintf(settings.client_name, sizeof(settings.client_name), "xtc-smoke-%ld",
|
||||
static_cast<long>(getpid()));
|
||||
settings.main_x = 0;
|
||||
settings.main_y = 0;
|
||||
settings.width = 1280;
|
||||
settings.height = 800;
|
||||
settings.transport_detached = settings.sequencer_detached = settings.graph_detached =
|
||||
settings.mixer_detached = 0;
|
||||
settings.loop_enabled = 0;
|
||||
settings.stop_after_tail = 0;
|
||||
std::string config = directory.path + "/preferences.conf";
|
||||
REQUIRE(xtc_settings_save(&settings, config.c_str()) == XTC_OK);
|
||||
Probe probe;
|
||||
jack_status_t status{};
|
||||
probe.client = jack_client_open("xtc-playback-probe", JackNoStartServer, &status);
|
||||
REQUIRE(probe.client);
|
||||
probe.ports[0] =
|
||||
jack_port_register(probe.client, "left", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||
probe.ports[1] =
|
||||
jack_port_register(probe.client, "right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||
REQUIRE(probe.ports[0] && probe.ports[1]);
|
||||
REQUIRE(jack_set_process_callback(probe.client, Probe::process, &probe) == 0 &&
|
||||
jack_activate(probe.client) == 0);
|
||||
MainWindow window(false, config.c_str());
|
||||
window.show();
|
||||
pump(100);
|
||||
REQUIRE(window.open_project("examples/projects/playback-test.xtc") == XTC_OK);
|
||||
auto *s = timeline(window);
|
||||
REQUIRE(s);
|
||||
click(window, find(window, "Play"));
|
||||
pump(300);
|
||||
if (!find(window, "running", false))
|
||||
{
|
||||
if (auto *error = find(window, "Start failed", false))
|
||||
std::fprintf(stderr, "Observed: %s\n", error->label());
|
||||
}
|
||||
REQUIRE(find(window, "running", false));
|
||||
REQUIRE(find(window, "MIDI device unavailable", false));
|
||||
for (int channel = 0; channel < 2; ++channel)
|
||||
{
|
||||
std::string source =
|
||||
std::string(settings.client_name) + (channel ? ":output_right" : ":output");
|
||||
int code = -1;
|
||||
for (int attempt = 0; attempt < 50; ++attempt)
|
||||
{
|
||||
code = jack_connect(probe.client, source.c_str(),
|
||||
jack_port_name(probe.ports[channel]));
|
||||
if (code == 0 || code == EEXIST)
|
||||
break;
|
||||
pump(10);
|
||||
}
|
||||
REQUIRE(code == 0 || code == EEXIST);
|
||||
}
|
||||
auto start = s->position();
|
||||
pump(700);
|
||||
REQUIRE(s->position() > start);
|
||||
REQUIRE(probe.heard[0].load() && probe.heard[1].load() && !probe.invalid.load());
|
||||
click(window, find(window, "Stop"));
|
||||
pump(300);
|
||||
auto paused = s->position();
|
||||
pump(200);
|
||||
REQUIRE(s->position() == paused);
|
||||
// Space resumes after a sequencer child has taken focus.
|
||||
s->take_focus();
|
||||
REQUIRE(static_cast<juce::Component *>(s)->keyPressed(juce::KeyPress(' ')));
|
||||
pump(300);
|
||||
REQUIRE(s->position() > paused);
|
||||
// A child at local (0,0) must paint consistently when composed into
|
||||
// the full workstation; this catches retained-coordinate origin errors.
|
||||
xtc_track_info track{};
|
||||
REQUIRE(xtc_project_get_track(s->project(), 0, &track) == XTC_OK);
|
||||
auto *header = s->track_widget(track.id)->header();
|
||||
auto *menu = window.child(0);
|
||||
REQUIRE(header->isShowing() && menu->isShowing());
|
||||
mouse_click(header, {14, 12});
|
||||
pump(60);
|
||||
REQUIRE(s->track_expanded(track.id));
|
||||
auto menu_image = menu->createComponentSnapshot(menu->getLocalBounds());
|
||||
auto header_image = header->createComponentSnapshot(header->getLocalBounds());
|
||||
auto image = window.createComponentSnapshot(window.getLocalBounds());
|
||||
REQUIRE(image.getPixelAt(2, 2) == menu_image.getPixelAt(2, 2));
|
||||
REQUIRE(image.getPixelAt(header->x() + 5, header->y() + 5) ==
|
||||
header_image.getPixelAt(5, 5));
|
||||
if (const char *capture = std::getenv("XTC_JUCE_SCREENSHOT"))
|
||||
{
|
||||
juce::FileOutputStream stream{juce::File(capture)};
|
||||
REQUIRE(stream.openedOk());
|
||||
juce::PNGImageFormat png;
|
||||
REQUIRE(png.writeImageToStream(image, stream));
|
||||
}
|
||||
window.do_callback();
|
||||
REQUIRE(!window.shown());
|
||||
xtc_settings saved{};
|
||||
REQUIRE(xtc_settings_load(&saved, config.c_str()) == XTC_OK);
|
||||
REQUIRE(!std::strcmp(saved.midi_input_port, settings.midi_input_port) &&
|
||||
!std::strcmp(saved.midi_output_port, settings.midi_output_port));
|
||||
std::puts("Live Play/Stop/Space and stereo playback passed with unavailable saved MIDI "
|
||||
"devices; preferences retained.");
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception &error)
|
||||
{
|
||||
std::fprintf(stderr, "Playback regression failed: %s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue