321 lines
16 KiB
C++
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;
|
|
}
|