Files
reasampler/tests/test_tail_control.cpp
T

189 lines
7.8 KiB
C++

// 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 <cstdio>
#include <string>
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 testSerializeByteIdentity() {
// Q-W1 structural-dedupe guard: the core/json-backed writer must emit the
// EXACT bytes the former snprintf writer produced ({"mode":%d,"manualMs":%.17g})
// and re-serializing a round-tripped setting must be byte-identical — the blob
// lives in the .rpp, so a byte shift would dirty every saved project.
TailSetting s; // None + 2000.0 default
CHECK(serializeTailSetting(s) == "{\"mode\":0,\"manualMs\":2000}");
TailSetting man; man.mode = TailMode::Manual; man.manualMs = 3141.592653589793;
const std::string json = serializeTailSetting(man);
auto back = deserializeTailSetting(json);
CHECK(back.has_value());
CHECK(back && serializeTailSetting(*back) == json); // stable second round-trip
}
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();
testSerializeByteIdentity();
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;
}