// 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 #include 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.cpp 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"); } // Distinctness is what keeps the two dispatch hooks from both claiming one command: // hookcommand owns the Main id, hookcommand2 owns the Media Explorer id. A suffix // collapse would silently double-fire the import. CHECK(mainId != mxId); // Cross-channel isolation: this build's ids must NOT collide with the other channel's, // or a beta install would rebind the stable keymap's entries. Asserted against the // opposite channel's literals, which is the collision that would actually occur. if (isBeta()) { CHECK(mainId != "CEREBELLUM_REASAMPLER_INGEST_IMPORT_MEDIA_EXPLORER"); CHECK(mxId != "CEREBELLUM_REASAMPLER_INGEST_IMPORT_MEDIA_EXPLORER_MX"); } else { CHECK(mainId != "CEREBELLUM_REASAMPLER_BETA_INGEST_IMPORT_MEDIA_EXPLORER"); CHECK(mxId != "CEREBELLUM_REASAMPLER_BETA_INGEST_IMPORT_MEDIA_EXPLORER_MX"); } } 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; }