mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-08-11 19:29:32 +00:00
Clean release snapshot
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
// e2e: drive a real SandboxedProcessor (host side) that spawns the real
|
||||
// slopsmith-vst-host child, which loads the passthrough VST3 and processes
|
||||
// audio over the shm ring. Proves the whole POSIX runtime: posix_spawn + fd
|
||||
// inheritance + ready handshake + prepare + audio round-trip + state + shutdown.
|
||||
//
|
||||
// argv[1] = path to slopsmith-vst-host
|
||||
// argv[2] = path to SlopPassThrough.vst3
|
||||
#include "Sandbox/SandboxedProcessor.h"
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
|
||||
using namespace slopsmith::sandbox;
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void check(bool c, const char* what, int line)
|
||||
{
|
||||
if (c) { ++g_pass; return; }
|
||||
++g_fail; std::fprintf(stderr, " FAIL: %s (line %d)\n", what, line);
|
||||
}
|
||||
#define CHECK(c) check((c), #c, __LINE__)
|
||||
|
||||
static bool allClose(const juce::AudioBuffer<float>& b, float v)
|
||||
{
|
||||
for (int ch = 0; ch < b.getNumChannels(); ++ch)
|
||||
for (int i = 0; i < b.getNumSamples(); ++i)
|
||||
if (std::abs(b.getSample(ch, i) - v) > 1.0e-4f) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc < 3) { std::fprintf(stderr, "usage: e2e_test <vst-host> <plugin.vst3>\n"); return 2; }
|
||||
|
||||
SandboxedProcessor::SpawnConfig cfg;
|
||||
cfg.pluginPath = juce::String::fromUTF8(argv[2]);
|
||||
cfg.pluginName = "PassThrough";
|
||||
cfg.sandboxExePath = juce::String::fromUTF8(argv[1]);
|
||||
cfg.audio.sampleRate = 48000;
|
||||
cfg.audio.maxBlockSamples = 256;
|
||||
cfg.audio.maxChannels = 2;
|
||||
cfg.audio.maxBlocks = 4;
|
||||
cfg.spawnTimeoutMs = 20000;
|
||||
|
||||
std::printf("=== sandbox e2e: spawn → process → state → shutdown ===\n");
|
||||
juce::String err;
|
||||
auto sb = SandboxedProcessor::spawn(cfg, err);
|
||||
CHECK(sb != nullptr);
|
||||
if (!sb) { std::fprintf(stderr, "spawn failed: %s\n", err.toRawUTF8()); return 1; }
|
||||
CHECK(sb->isAlive());
|
||||
|
||||
#if JUCE_LINUX
|
||||
// Orphan-cleanup check (issue #265). When SLOPSMITH_E2E_LEAK_TEST is set,
|
||||
// simulate a host *crash*: exit RIGHT NOW via _Exit, skipping sb's
|
||||
// destructor — so no `shutdown` op and no SIGTERM→SIGKILL ladder ever runs.
|
||||
// The child must still die, via PR_SET_PDEATHSIG (installLinuxParentDeathSignal
|
||||
// in the child). The leak_test.sh wrapper reads the child pid from its log
|
||||
// and asserts it is gone after this parent vanishes.
|
||||
if (std::getenv("SLOPSMITH_E2E_LEAK_TEST") != nullptr)
|
||||
{
|
||||
std::printf("LEAK_TEST: child alive; crashing host without shutdown\n");
|
||||
std::fflush(stdout);
|
||||
std::_Exit(0);
|
||||
}
|
||||
#endif
|
||||
|
||||
sb->prepareToPlay(48000.0, 256);
|
||||
|
||||
juce::AudioBuffer<float> buf(2, 256);
|
||||
juce::MidiBuffer midi;
|
||||
|
||||
// Pace at one block period (256 samples @ 48 kHz ≈ 5.33 ms, rounded up to
|
||||
// 6 ms) so the host doesn't outrun the sandbox worker — a faster cadence
|
||||
// would let the host's pop legitimately time out and read silence.
|
||||
constexpr int kBlockPeriodMs = 6;
|
||||
|
||||
// A single constant level feeds both the warm-up and the steady-state loop.
|
||||
// The sandbox is two independent rings (input, output), so it promises
|
||||
// *bounded latency*, NOT exact per-block phase: if any block's round-trip
|
||||
// overruns the pop timeout, the host inserts silence and moves on while the
|
||||
// worker still produces that block's output, which shifts every later read
|
||||
// one slot late. A distinct-per-block probe would then read the *previous*
|
||||
// block's (valid, non-silent) output and flag it as a spurious mismatch —
|
||||
// observed as a flaky "200 misvalued" on loaded CI runners. A constant
|
||||
// level is phase-invariant: a lagged read still equals 2×kLevel (correct),
|
||||
// a timed-out block is still silence (dropout), and a genuine scaling bug
|
||||
// still produces a wrong value. In-phase slot correctness with distinct
|
||||
// markers is covered by the deterministic standalone ring unit test.
|
||||
constexpr float kLevel = 0.3f;
|
||||
|
||||
// Warm-up: a plugin's first few processBlock calls (VST3 activation,
|
||||
// allocation, first-touch) can exceed one block period, so the sandbox
|
||||
// inserts silence for those by design. Discard a warm-up burst so the
|
||||
// steady-state assertions aren't measuring cold start.
|
||||
for (int n = 0; n < 40; ++n)
|
||||
{
|
||||
for (int ch = 0; ch < 2; ++ch)
|
||||
for (int i = 0; i < 256; ++i) buf.setSample(ch, i, kLevel);
|
||||
sb->processBlock(buf, midi);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kBlockPeriodMs));
|
||||
}
|
||||
|
||||
// Steady state. Each delivered block MUST be exactly 2×kLevel (a real
|
||||
// scaling bug surfaces as a wrong non-zero value → `misvalued`, which must
|
||||
// be zero). A block that times out under load is returned as silence by
|
||||
// SandboxedProcessor (by design) → counted as a dropout, tolerated in small
|
||||
// numbers since a shared CI runner can stall a single round-trip past the
|
||||
// pop timeout even when the runtime is correct.
|
||||
int correct = 0, dropouts = 0, misvalued = 0;
|
||||
constexpr int kBlocks = 200;
|
||||
for (int n = 0; n < kBlocks; ++n)
|
||||
{
|
||||
for (int ch = 0; ch < 2; ++ch)
|
||||
for (int i = 0; i < 256; ++i) buf.setSample(ch, i, kLevel);
|
||||
sb->processBlock(buf, midi);
|
||||
if (allClose(buf, kLevel * 2.0f)) ++correct;
|
||||
else if (allClose(buf, 0.0f)) ++dropouts; // timed-out → silence
|
||||
else ++misvalued; // wrong value → real bug
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kBlockPeriodMs));
|
||||
}
|
||||
std::printf(" steady-state: %d correct, %d dropouts, %d misvalued (of %d)\n",
|
||||
correct, dropouts, misvalued, kBlocks);
|
||||
CHECK(misvalued == 0); // every delivered block is exact
|
||||
CHECK(correct >= kBlocks * 9 / 10); // overwhelmingly delivered (tolerate CI jitter)
|
||||
|
||||
// State round-trip: child returns the plugin's getStateInformation blob.
|
||||
juce::MemoryBlock state;
|
||||
sb->getStateInformation(state);
|
||||
CHECK(state.getSize() > 0);
|
||||
sb->setStateInformation(state.getData(), (int)state.getSize());
|
||||
CHECK(sb->isAlive()); // setState shouldn't have torn the sandbox down
|
||||
|
||||
#if JUCE_MAC || JUCE_LINUX
|
||||
// Editor open/close protocol: the child opens a floating top-level editor
|
||||
// window in its own process (NSWindow on macOS, X11 window on Linux via
|
||||
// JUCE 8's VST3 IRunLoop hosting) and the host tracks only the open bit.
|
||||
// Proves the kOpenEditor round-trip + editorOpen tracking + kCloseEditor.
|
||||
// Runs under xvfb on the Linux CI runner; visual focus/DPI is the one thing
|
||||
// a headless runner can't verify (manual on real hardware).
|
||||
CHECK(sb->hasEditor());
|
||||
const bool opened = sb->requestOpenEditor();
|
||||
CHECK(opened);
|
||||
CHECK(sb->isEditorOpen());
|
||||
sb->requestCloseEditor();
|
||||
CHECK(!sb->isEditorOpen());
|
||||
CHECK(sb->isAlive()); // open/close must not crash the child
|
||||
#endif
|
||||
|
||||
sb.reset(); // destructor → shutdown op → SIGTERM ladder; must not hang
|
||||
|
||||
std::printf("\n%d passed, %d failed\n", g_pass, g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user