Files
reasampler/tests/test_app_version.cpp

321 lines
16 KiB
C++

// Standalone tests for reasampler::app_version — no REAPER, no framework. Covers the
// Phase V (V1) pure version-identity core: the exact-string fidelity of the CMake-sourced
// constant (leading-zero preserved), the semver parse/compare arithmetic a within-channel
// forward migration leans on, and the three-way writing-version classification persist reads
// back from ext state (PreVersioning / Unknown / Stamped). The ext-state I/O (persist) and
// the show-version action (main) are DAW-verified shell.
#include "../src/core/version/app_version.h"
// REAPER-free header; pulled in for the ingest action's FOREVER-STABLE id suffixes, so
// the composition assertions below check the strings that actually ship.
#include "../src/shell/actions/ingest.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::version;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// --- appVersion: exact-string fidelity, leading zero preserved ----------------
//
// The channel is a COMPILE-TIME fact (REASAMPLER_CHANNEL_IS_BETA, threaded through the same
// configure_file'd header): the stable `build` tree compiles these tests with the flag = 0,
// the `build-beta` tree with = 1. So each config's ctest run validates ITS OWN channel's
// derivation. The channel-dependent assertions below branch on isBeta() so the ONE test
// source is correct in both configs — and each branch would fail if the derivation regressed
// (a beta build omitting the suffix, or stable gaining one, trips it).
static void testVersionConstantRenderStructure() {
// stampVersion() is the CMake-sourced numeric triple on both channels (no suffix).
// It must be parseable — a malformed version string slipped in at CMake time would
// silently classify every saved project as Unknown on read-back.
CHECK(parseVersion(stampVersion()).has_value());
// appVersion() is the user-visible render: exactly stampVersion() on stable, and
// stampVersion() + "-beta" on beta. This is the derivation that must hold at ANY
// version, without naming a literal.
if (isBeta()) {
CHECK(appVersion() == stampVersion() + "-beta");
} else {
CHECK(appVersion() == stampVersion());
}
}
static void testLeadingZeroFidelity() {
// Leading-zero-fidelity is the whole point of sourcing the version STRING (not
// reconstructing from numeric components): a CMake normalize of "0.9.01" to "0.9.1"
// is a silent regression. This is tested with a SYNTHETIC fixed example so it pins
// the fidelity contract independent of any particular live version.
// parseVersion("0.9.01") must yield patch == 1 (not 01 as an integer, which is 1,
// but the important thing is the STRING "0.9.01" is accepted and produces the right
// triple — the string fidelity is the caller's job, the integer parse just rounds the
// patch correctly). The live version may or may not have a zero-padded patch; this
// synthetic pin catches a parse regression that would misclassify such a stamp.
auto v = parseVersion("0.9.01");
CHECK(v.has_value());
CHECK(v && v->major == 0 && v->minor == 9 && v->patch == 1); // "01" -> 1, not rejected
}
// --- channel-derived rendering (V4) -------------------------------------------
static void testChannelDerivedRendering() {
// The DISPLAY render. Stable: exactly the numeric string. Beta: numeric + "-beta"
// (a plain suffix, V2). This is the show-version + panel-readout value. Each branch is
// the assertion the OTHER config's build would need to fail — i.e. a stable build that
// wrongly rendered "-beta", or a beta build that dropped it, is caught here.
if (isBeta()) {
CHECK(channel() == Channel::Beta);
CHECK(appVersion() == stampVersion() + "-beta");
} else {
CHECK(channel() == Channel::Stable);
CHECK(appVersion() == stampVersion());
}
// The STAMP value is the numeric triple on BOTH channels — never suffixed — so it stays
// classifiable (see the stamp-classifiability test) and byte-identical to stable.
// Verified against stampVersion() rather than a literal so a version bump is not a
// test failure: the real invariant is that the stamp is parseable (checked in
// testVersionConstantRenderStructure) and that it has no suffix here.
CHECK(appVersion().substr(0, stampVersion().size()) == stampVersion());
CHECK(isBeta() ? appVersion().size() > stampVersion().size()
: appVersion().size() == stampVersion().size());
}
static void testChannelDerivedIdentityStrings() {
// Namespace, command-id prefix, action-name prefix, binary + dock idents all fork from
// the one channel bit. Stable values are BYTE-IDENTICAL to the pre-V4 build — any drift
// in the stable branch is a shipped-identity defect.
if (isBeta()) {
CHECK(extStateNamespace() == "reasampler_beta");
CHECK(commandIdPrefix() == "CEREBELLUM_REASAMPLER_BETA_");
CHECK(actionDisplayPrefix() == "ReaSampler beta: ");
CHECK(binaryName() == "reaper_reasampler_beta");
CHECK(dockTitle() == "ReaSampler Bank beta");
CHECK(dockIdent() == "reasampler_bank_panel_beta");
} else {
CHECK(extStateNamespace() == "reasampler");
CHECK(commandIdPrefix() == "CEREBELLUM_REASAMPLER_");
CHECK(actionDisplayPrefix() == "ReaSampler: ");
CHECK(binaryName() == "reaper_reasampler");
CHECK(dockTitle() == "ReaSampler Bank");
CHECK(dockIdent() == "reasampler_bank_panel");
}
}
static void testVstIdentityStringsForkByChannel() {
// S18: the VST3 instrument's on-disk name and display name fork from the SAME one channel
// bit as the extension's idents above. Stable values are BYTE-IDENTICAL to pre-S18 — any
// drift in the stable branch orphans a saved instance's on-disk reference / mislabels the
// FX browser. These are the accessors vst_entry.cpp's factory, the editor title, and the
// embed label all source; each branch is the assertion the OTHER config's build would fail.
if (isBeta()) {
CHECK(vstOutputName() == "reasampler_9000_beta");
CHECK(vstPluginName() == "ReaSampler 9000 beta");
} else {
CHECK(vstOutputName() == "reasampler_9000");
CHECK(vstPluginName() == "ReaSampler 9000");
}
// The on-disk name must match the CMake OUTPUT_NAME fork (REASAMPLER_VST_OUTPUT_NAME): a
// divergence between this accessor and the artifact name would ship a binary whose
// self-identification disagrees with its filename. (The CMake side is the authoritative
// artifact name; this pins the in-binary derivation to the same two literals.)
CHECK(vstOutputName() == (isBeta() ? "reasampler_9000_beta" : "reasampler_9000"));
}
static void testVstIdentityAndDataNamespaceShareOneChannel() {
// The S18 pairing invariant at the SEAM: the VST's plugin identity (its display/output
// names) and the DATA namespace its bridge reads (extStateNamespace(), what ext_keys
// delegates to) must resolve to the SAME channel — a beta-named plugin reading the stable
// namespace, or vice versa, is precisely the split the invariant forbids. Both forks fan
// out from the one isBeta() bit, so this assertion fails if EITHER fork regressed
// independently (a beta output name paired with the stable namespace trips the beta arm).
const bool identityIsBeta =
(vstPluginName() == "ReaSampler 9000 beta") && (vstOutputName() == "reasampler_9000_beta");
const bool dataIsBeta = (extStateNamespace() == "reasampler_beta");
CHECK(identityIsBeta == dataIsBeta); // identity and data agree on the channel
CHECK(identityIsBeta == isBeta()); // and both agree with the compiled bit
}
static void testChannelQualifiedIdAndNameComposition() {
// The two composition helpers the shells funnel through. A representative shipped id
// (CAPTURE_TRACK) and phrase must compose to the exact channel-qualified strings — this
// is what guarantees stable rebuilds its shipped id and beta gets the isolated one.
if (isBeta()) {
CHECK(channelCommandId("CAPTURE_TRACK") == "CEREBELLUM_REASAMPLER_BETA_CAPTURE_TRACK");
CHECK(channelActionName("capture selected track(s)") ==
"ReaSampler beta: capture selected track(s)");
} else {
CHECK(channelCommandId("CAPTURE_TRACK") == "CEREBELLUM_REASAMPLER_CAPTURE_TRACK");
CHECK(channelActionName("capture selected track(s)") ==
"ReaSampler: capture selected track(s)");
}
}
static void testMediaExplorerImportIdsAreDistinctAndChannelIsolated() {
// The Media-Explorer import publishes into TWO action sections, and a custom_action
// idStr must be unique across all sections — so the two entries carry two suffixes.
// Both are FOREVER-STABLE per channel. Composed from the SHIPPED constants and checked
// against spelled-out literals, so a suffix edit in ingest.h fails here.
const std::string mainId = channelCommandId(kIngestImportMediaExplorerId);
const std::string mxId = channelCommandId(kIngestImportMediaExplorerMxId);
if (isBeta()) {
CHECK(mainId == "CEREBELLUM_REASAMPLER_BETA_INGEST_IMPORT_MEDIA_EXPLORER");
CHECK(mxId == "CEREBELLUM_REASAMPLER_BETA_INGEST_IMPORT_MEDIA_EXPLORER_MX");
CHECK(channelActionName("import Media Explorer file into selected track") ==
"ReaSampler beta: import Media Explorer file into selected track");
} else {
CHECK(mainId == "CEREBELLUM_REASAMPLER_INGEST_IMPORT_MEDIA_EXPLORER");
CHECK(mxId == "CEREBELLUM_REASAMPLER_INGEST_IMPORT_MEDIA_EXPLORER_MX");
CHECK(channelActionName("import Media Explorer file into selected track") ==
"ReaSampler: import Media Explorer file into selected track");
}
}
static void testStampClassifiesAsStampedOnOwnChannel() {
// The V4 stamp-classifiability requirement: the value a channel WRITES (stampVersion())
// must classify as Stamped when that same channel reads it back — on BOTH channels. A
// "-beta"-suffixed stamp would classify as Unknown, so this fails if a build ever stamped
// appVersion() instead of stampVersion().
WritingVersion wv = classifyWritingVersion(stampVersion());
CHECK(wv.kind == WritingVersion::Kind::Stamped);
// raw must be the exact stamp string (no truncation, no suffix added by classify).
CHECK(wv.raw == stampVersion());
// parsed must round-trip: re-parsing the stamp string must yield the same triple
// classify stored, without naming a literal version.
auto reparsed = parseVersion(stampVersion());
CHECK(reparsed.has_value());
CHECK(reparsed && wv.parsed.major == reparsed->major
&& wv.parsed.minor == reparsed->minor
&& wv.parsed.patch == reparsed->patch);
// The display string with the "-beta" suffix is intentionally NOT the stamp value —
// confirm it classifies Unknown, proving why the stamp must stay the numeric triple.
// This uses a synthetic fixed string (not the live version) because the intent is to
// show that ANY "-beta"-suffixed string fails the semver parse used by classify.
CHECK(classifyWritingVersion("0.9.01-beta").kind == WritingVersion::Kind::Unknown);
}
// --- parseVersion: well-formed, leading zeros, and rejection ------------------
static void testParseWellFormed() {
auto v = parseVersion("0.9.01");
CHECK(v.has_value());
CHECK(v && v->major == 0 && v->minor == 9 && v->patch == 1); // "01" -> 1
auto w = parseVersion("1.10.2");
CHECK(w.has_value());
CHECK(w && w->major == 1 && w->minor == 10 && w->patch == 2);
}
static void testParseRejectsMalformed() {
CHECK(!parseVersion("").has_value()); // empty
CHECK(!parseVersion("1.2").has_value()); // too few components
CHECK(!parseVersion("1.2.3.4").has_value()); // too many components
CHECK(!parseVersion("1..2").has_value()); // empty middle component
CHECK(!parseVersion(".1.2").has_value()); // empty leading component
CHECK(!parseVersion("1.2.").has_value()); // empty trailing component
CHECK(!parseVersion("1.2.x").has_value()); // non-digit
CHECK(!parseVersion("-1.2.3").has_value()); // sign
CHECK(!parseVersion("1.2.3-beta").has_value()); // trailing garbage
CHECK(!parseVersion("v1.2.3").has_value()); // prefix
CHECK(!parseVersion(" 1.2.3").has_value()); // leading whitespace
}
// --- versionLess: NUMERIC ordering (10 > 9, not lexicographic) ----------------
static void testOrderingByPatch() {
// 0.9.02 > 0.9.01 (the per-test-build patch increment the spec calls out).
auto a = parseVersion("0.9.01");
auto b = parseVersion("0.9.02");
CHECK(a && b);
CHECK(a && b && versionLess(*a, *b));
CHECK(a && b && !versionLess(*b, *a));
}
static void testOrderingIsNumericNotLexicographic() {
// 0.10.01 > 0.9.02 — the case a string compare would get WRONG ("10" < "9"
// lexicographically). This assertion fails if compare regressed to string order.
auto a = parseVersion("0.9.02");
auto b = parseVersion("0.10.01");
CHECK(a && b);
CHECK(a && b && versionLess(*a, *b));
// And a major bump outranks a large minor: 1.0.0 > 0.99.99.
auto c = parseVersion("0.99.99");
auto d = parseVersion("1.0.0");
CHECK(c && d && versionLess(*c, *d));
}
static void testOrderingIrreflexive() {
// Equal versions are not less-than either way.
auto a = parseVersion("0.9.01");
auto b = parseVersion("0.9.01");
CHECK(a && b && !versionLess(*a, *b));
CHECK(a && b && !versionLess(*b, *a));
}
// --- classifyWritingVersion: the three-way stamp classification ----------------
static void testAbsentStampIsPreVersioning() {
// An absent key -> getProjExtStateString returns "" -> PreVersioning (a project
// saved before this feature). Silent, not an error, no throw.
WritingVersion wv = classifyWritingVersion("");
CHECK(wv.kind == WritingVersion::Kind::PreVersioning);
CHECK(wv.raw.empty());
}
static void testMalformedStampIsUnknown() {
// A present-but-unparseable stamp -> Unknown (ignored, never a throw). Keeps the raw
// value for diagnostics but does not pretend it is a version.
WritingVersion wv = classifyWritingVersion("garbage-not-a-version");
CHECK(wv.kind == WritingVersion::Kind::Unknown);
CHECK(wv.raw == "garbage-not-a-version");
}
static void testWellFormedStampIsStamped() {
// A well-formed stamp -> Stamped, carrying the EXACT stored string plus the parsed
// triple for comparison. Round-trips the current version through classify.
WritingVersion wv = classifyWritingVersion("0.9.01");
CHECK(wv.kind == WritingVersion::Kind::Stamped);
CHECK(wv.raw == "0.9.01");
CHECK(wv.parsed.major == 0 && wv.parsed.minor == 9 && wv.parsed.patch == 1);
}
static void testStampedStampIsOrderableAgainstCurrent() {
// The migration use case: a stamp read back is comparable to the running build. An
// older stamp (0.9.01) precedes a newer one (0.9.05) numerically.
WritingVersion older = classifyWritingVersion("0.9.01");
WritingVersion newer = classifyWritingVersion("0.9.05");
CHECK(older.kind == WritingVersion::Kind::Stamped);
CHECK(newer.kind == WritingVersion::Kind::Stamped);
CHECK(versionLess(older.parsed, newer.parsed));
}
int main() {
testVersionConstantRenderStructure();
testLeadingZeroFidelity();
testChannelDerivedRendering();
testChannelDerivedIdentityStrings();
testVstIdentityStringsForkByChannel();
testVstIdentityAndDataNamespaceShareOneChannel();
testChannelQualifiedIdAndNameComposition();
testMediaExplorerImportIdsAreDistinctAndChannelIsolated();
testStampClassifiesAsStampedOnOwnChannel();
testParseWellFormed();
testParseRejectsMalformed();
testOrderingByPatch();
testOrderingIsNumericNotLexicographic();
testOrderingIrreflexive();
testAbsentStampIsPreVersioning();
testMalformedStampIsUnknown();
testWellFormedStampIsStamped();
testStampedStampIsOrderableAgainstCurrent();
if (g_fail == 0) std::printf("app_version: all tests passed\n");
else std::printf("app_version: %d CHECK(s) FAILED\n", g_fail);
return g_fail == 0 ? 0 : 1;
}