// Standalone tests for reasampler::tail_control — no REAPER, no framework. Covers // the pure pieces behind the docked panel's tail-mode toggle: the cycle order // (None -> Auto -> Manual -> None), the manual-length clamp to the 8 s cap, and the // toggle label text. The drawing / click hit-testing is DAW-verified in bank_panel. #include "../src/tail_control.h" #include #include using namespace reasampler; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // --- cycleTailMode: the toggle order ------------------------------------------ static void testCycleOrderIsNoneAutoManualNone() { // None -> Auto -> Manual -> None, wrapping. The click handler relies on exactly // this order; a reorder (e.g. skipping Manual) would fail here. CHECK(cycleTailMode(TailMode::None) == TailMode::Auto); CHECK(cycleTailMode(TailMode::Auto) == TailMode::Manual); CHECK(cycleTailMode(TailMode::Manual) == TailMode::None); } static void testCycleThreeStepsReturnsToStart() { // Three cycles from any state land back on that state (a full lap of the 3-cycle). TailMode m = TailMode::None; m = cycleTailMode(m); m = cycleTailMode(m); m = cycleTailMode(m); CHECK(m == TailMode::None); } // --- clampManualMs: the runaway guard ----------------------------------------- static void testManualClampInRangeIsUnchanged() { // A value inside [0, kMaxTailMs] passes through untouched. CHECK(clampManualMs(kDefaultManualTailMs) == kDefaultManualTailMs); CHECK(clampManualMs(0.0) == 0.0); CHECK(clampManualMs(kMaxTailMs) == kMaxTailMs); } static void testManualClampCapsAtEightSeconds() { // Over the 8 s cap clamps to kMaxTailMs; negative floors to 0. This mirrors the // clamp in tailRenderSettingsFor, so the panel and the render agree on the bound. CHECK(clampManualMs(kMaxTailMs + 5000.0) == kMaxTailMs); CHECK(clampManualMs(-100.0) == 0.0); } // --- adjustManualMs: the scroll-wheel fine-adjust arithmetic ------------------ static void testAdjustUpAndDownBySteps() { // Positive notches lengthen, negative shorten, in whole kManualStepMs increments. CHECK(adjustManualMs(2000.0, 1, kManualStepMs) == 2250.0); CHECK(adjustManualMs(2000.0, -1, kManualStepMs) == 1750.0); CHECK(adjustManualMs(2000.0, 4, kManualStepMs) == 3000.0); // 4 * 250 CHECK(adjustManualMs(2000.0, 0, kManualStepMs) == 2000.0); // no notch, no move } static void testAdjustClampsAtUpperBound() { // Scrolling up past the 8 s cap saturates AT the cap, never beyond. CHECK(adjustManualMs(kMaxTailMs, 1, kManualStepMs) == kMaxTailMs); CHECK(adjustManualMs(kMaxTailMs - 100.0, 10, kManualStepMs) == kMaxTailMs); } static void testAdjustClampsAtLowerBound() { // Scrolling down past 0 floors at 0, never negative. CHECK(adjustManualMs(0.0, -1, kManualStepMs) == 0.0); CHECK(adjustManualMs(100.0, -10, kManualStepMs) == 0.0); } // --- tailToggleLabel: the exact strings the panel draws ----------------------- static void testLabelStringsPerMode() { TailSetting off; off.mode = TailMode::None; TailSetting autoM; autoM.mode = TailMode::Auto; // Off/Auto carry NO length regardless of manualMs. off.manualMs = 5000.0; autoM.manualMs = 5000.0; CHECK(tailToggleLabel(off) == "Tail: Off"); CHECK(tailToggleLabel(autoM) == "Tail: Auto"); } static void testManualLabelRendersLengthInSeconds() { // Manual appends the length in seconds to one decimal — pin the format and the // boundary values (0.0s, the 2 s default, the 8 s cap). TailSetting man; man.mode = TailMode::Manual; man.manualMs = 0.0; CHECK(tailToggleLabel(man) == "Tail: Manual 0.0s"); man.manualMs = kDefaultManualTailMs; // 2000 ms CHECK(tailToggleLabel(man) == "Tail: Manual 2.0s"); man.manualMs = kMaxTailMs; // 8000 ms CHECK(tailToggleLabel(man) == "Tail: Manual 8.0s"); // An over-cap stored value renders at the CLAMPED length, never past the cap. man.manualMs = kMaxTailMs + 3000.0; CHECK(tailToggleLabel(man) == "Tail: Manual 8.0s"); } static void testDefaultSettingIsOff() { // The zero-value setting is None (Off) with the 2 s manual default — the safe // default the panel starts in so captures stay exact-bounds until opt-in. TailSetting s; CHECK(s.mode == TailMode::None); CHECK(s.manualMs == kDefaultManualTailMs); CHECK(tailToggleLabel(s) == "Tail: Off"); } // --- serialize/deserialize: per-project persistence round-trip ---------------- static bool settingsEqual(const TailSetting& a, const TailSetting& b) { return a.mode == b.mode && a.manualMs == b.manualMs; } static void testRoundTripNoneDefault() { TailSetting s; // None + 2 s default auto back = deserializeTailSetting(serializeTailSetting(s)); CHECK(back.has_value()); CHECK(back && settingsEqual(*back, s)); } static void testRoundTripManualArbitraryMs() { // A non-round manual length must round-trip bit-for-bit (17-sig-digit emit). TailSetting s; s.mode = TailMode::Manual; s.manualMs = 3141.592653589793; auto back = deserializeTailSetting(serializeTailSetting(s)); CHECK(back.has_value()); CHECK(back && settingsEqual(*back, s)); } static void testRoundTripAuto() { TailSetting s; s.mode = TailMode::Auto; s.manualMs = 500.0; auto back = deserializeTailSetting(serializeTailSetting(s)); CHECK(back.has_value()); CHECK(back && settingsEqual(*back, s)); } static void testDeserializeEmptyIsDefault() { // An absent/empty stored value (older project) -> nullopt, so the caller falls // back to the default. This is the graceful-old-project path the brief requires. CHECK(!deserializeTailSetting("").has_value()); } static void testDeserializeMalformedIsDefault() { // Garbage, a missing key, or an unknown mode enumerant -> nullopt (no crash). CHECK(!deserializeTailSetting("not json at all").has_value()); CHECK(!deserializeTailSetting("{\"mode\":1}").has_value()); // manualMs missing CHECK(!deserializeTailSetting("{\"manualMs\":2000}").has_value()); // mode missing CHECK(!deserializeTailSetting("{\"mode\":9,\"manualMs\":2000}").has_value()); // bad enum CHECK(!deserializeTailSetting("{\"mode\":x,\"manualMs\":2000}").has_value()); // non-numeric } int main() { testCycleOrderIsNoneAutoManualNone(); testCycleThreeStepsReturnsToStart(); testManualClampInRangeIsUnchanged(); testManualClampCapsAtEightSeconds(); testAdjustUpAndDownBySteps(); testAdjustClampsAtUpperBound(); testAdjustClampsAtLowerBound(); testLabelStringsPerMode(); testManualLabelRendersLengthInSeconds(); testDefaultSettingIsOff(); testRoundTripNoneDefault(); testRoundTripManualArbitraryMs(); testRoundTripAuto(); testDeserializeEmptyIsDefault(); testDeserializeMalformedIsDefault(); if (g_fail == 0) std::printf("tail_control: all tests passed\n"); else std::printf("tail_control: %d CHECK(s) FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }