feat(version): single-source semver, ext-state stamp, show-version action

Version "0.9.01" sourced once from CMake via configure_file'd header; pure
app_version module (parse/compare/classify) with CTest. Stamp rides the persist
save seam under "reasampler"/"version"; absent stamp reads as pre-versioning.
On-demand "show version" action, no startup print.
This commit is contained in:
2026-07-26 15:16:55 -04:00
parent 791a9c60eb
commit f6dddbf5a3
8 changed files with 409 additions and 2 deletions
+40 -2
View File
@@ -1,10 +1,30 @@
cmake_minimum_required(VERSION 3.19) cmake_minimum_required(VERSION 3.19)
project(reaper_reasampler LANGUAGES CXX)
# ---------------------------------------------------------------------------
# Version — SINGLE SOURCE OF TRUTH (Phase V, V1). Edit REASAMPLER_VERSION here and
# nowhere else: it flows to the binary constant, the ext-state writing-version stamp,
# and the "show version" action via a configure_file'd header (below). The string is
# authoritative verbatim — leading zero preserved (Daniel-fixed: exactly "0.9.01",
# two-digit zero-padded patch). We deliberately do NOT reconstruct the display string
# from project(VERSION)'s numeric components, since CMake may normalize a numeric patch
# field; the string variable is what renders. project(VERSION ...) is still set (with a
# normalized 0.9.1 triple) for CMake hygiene / any downstream numeric use, but it is NOT
# the rendered source of truth.
set(REASAMPLER_VERSION "0.9.01")
project(reaper_reasampler VERSION 0.9.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# Generate version_generated.h from the one REASAMPLER_VERSION variable. Regenerated at
# configure time whenever the variable changes; the exact string is substituted verbatim.
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/src/version_generated.h.in
${CMAKE_CURRENT_BINARY_DIR}/generated/version_generated.h
@ONLY)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Vendored dependencies — add as git submodules (see README): # Vendored dependencies — add as git submodules (see README):
# git submodule add https://github.com/justinfrankel/reaper-sdk vendor/reaper-sdk # git submodule add https://github.com/justinfrankel/reaper-sdk vendor/reaper-sdk
@@ -195,6 +215,19 @@ add_library(wav_trim STATIC src/wav_trim.cpp)
target_include_directories(wav_trim PUBLIC src) target_include_directories(wav_trim PUBLIC src)
target_link_libraries(wav_trim PUBLIC peaks) target_link_libraries(wav_trim PUBLIC peaks)
# ---------------------------------------------------------------------------
# 2i) Pure app_version library — NO REAPER, NO SWELL. The Phase V (V1) version-identity
# core: re-exports the ONE CMake-sourced version string (via the configure_file'd
# version_generated.h) and owns the pure parse/compare/classify logic — the semver
# ordering a within-channel forward migration needs, and the three-way writing-version
# classification (PreVersioning / Unknown / Stamped) persist reads back from ext state.
# Split out so the exact-string fidelity + parse/compare are unit-tested outside the
# DAW; the ext-state write/read (persist) and the show-version action (main) are shell.
# Depends on the generated header in the build tree (PUBLIC so every consumer sees it).
# ---------------------------------------------------------------------------
add_library(app_version STATIC src/app_version.cpp)
target_include_directories(app_version PUBLIC src ${CMAKE_CURRENT_BINARY_DIR}/generated)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 3) Standalone tests for the pure modules (run without launching REAPER). # 3) Standalone tests for the pure modules (run without launching REAPER).
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -267,6 +300,10 @@ add_executable(owned_manifest_tests tests/test_owned_manifest.cpp)
target_link_libraries(owned_manifest_tests PRIVATE owned_manifest) target_link_libraries(owned_manifest_tests PRIVATE owned_manifest)
add_test(NAME owned_manifest_tests COMMAND owned_manifest_tests) add_test(NAME owned_manifest_tests COMMAND owned_manifest_tests)
add_executable(app_version_tests tests/test_app_version.cpp)
target_link_libraries(app_version_tests PRIVATE app_version)
add_test(NAME app_version_tests COMMAND app_version_tests)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked). # 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -303,8 +340,9 @@ add_library(reaper_reasampler MODULE
src/actions.cpp src/actions.cpp
src/bank_book.cpp src/bank_book.cpp
src/owned_manifest.cpp src/owned_manifest.cpp
src/app_version.cpp
) )
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings tail_control realtime_record bank_book wav_trim owned_manifest) target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings tail_control realtime_record bank_book wav_trim owned_manifest app_version)
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC}) target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
set_target_properties(reaper_reasampler PROPERTIES PREFIX "" OUTPUT_NAME "reaper_reasampler") set_target_properties(reaper_reasampler PROPERTIES PREFIX "" OUTPUT_NAME "reaper_reasampler")
+76
View File
@@ -0,0 +1,76 @@
// app_version.cpp — implementation of the pure version-identity core (Phase V, V1).
// See app_version.h for the contract. The version STRING itself comes from
// version_generated.h (produced by CMake configure_file from the one REASAMPLER_VERSION
// variable) — this TU just re-exports it and owns the pure parse/compare/classify logic.
#include "app_version.h"
#include <string>
#include "version_generated.h" // REASAMPLER_VERSION_STRING — configure_file'd from CMake
namespace reasampler {
const std::string& appVersion() {
// Function-local static: initialized once from the compile-time string, returned by
// const ref so callers share the one authoritative instance. The macro is the exact
// CMake value, leading zero and all.
static const std::string kVersion = REASAMPLER_VERSION_STRING;
return kVersion;
}
std::optional<Version> parseVersion(const std::string& s) {
// Split on '.' into exactly three non-empty all-digit components. No sign, no
// whitespace, no trailing garbage. Leading zeros are allowed (0.9.01 parses to
// {0,9,1}) — the display fidelity of the zero is the string's job, not this parse's.
Version v;
int component = 0; // 0=major, 1=minor, 2=patch
long long acc = 0; // current component accumulator (long long guards overflow)
bool digitsInComponent = false;
int* const out[3] = {&v.major, &v.minor, &v.patch};
for (char c : s) {
if (c == '.') {
if (!digitsInComponent) return std::nullopt; // empty component (".", "1..2")
if (component >= 2) return std::nullopt; // too many dots
*out[component] = static_cast<int>(acc);
++component;
acc = 0;
digitsInComponent = false;
continue;
}
if (c < '0' || c > '9') return std::nullopt; // non-digit (sign, letter, ws)
acc = acc * 10 + (c - '0');
if (acc > 1'000'000'000LL) return std::nullopt; // absurdly large -> reject
digitsInComponent = true;
}
if (component != 2 || !digitsInComponent) return std::nullopt; // too few components
*out[2] = static_cast<int>(acc);
return v;
}
bool versionLess(const Version& a, const Version& b) {
if (a.major != b.major) return a.major < b.major;
if (a.minor != b.minor) return a.minor < b.minor;
return a.patch < b.patch;
}
WritingVersion classifyWritingVersion(const std::string& rawStamp) {
WritingVersion wv;
if (rawStamp.empty()) {
wv.kind = WritingVersion::Kind::PreVersioning; // no stamp -> pre-versioning
return wv;
}
std::optional<Version> parsed = parseVersion(rawStamp);
if (!parsed) {
wv.kind = WritingVersion::Kind::Unknown; // present but malformed -> ignored
wv.raw = rawStamp;
return wv;
}
wv.kind = WritingVersion::Kind::Stamped;
wv.raw = rawStamp;
wv.parsed = *parsed;
return wv;
}
} // namespace reasampler
+73
View File
@@ -0,0 +1,73 @@
#pragma once
// app_version — the REAPER-free version-identity core (Phase V, V1). The single
// source of truth for the version STRING lives in CMake (a `REASAMPLER_VERSION`
// variable threaded in via configure_file -> version_generated.h); this module
// re-exports it as the canonical constant and owns every pure operation on it: the
// exact-string render, the parse/compare arithmetic a within-channel forward
// migration will lean on, and the "which version wrote this project" result that
// persist reads back from ext state (absent stamp = pre-versioning, never an error).
//
// PURE MODULE: NO REAPER types, NO SWELL, NO vendor/ includes. Standard library
// only. Builds and unit-tests without REAPER (mirror of bank_model / tail_control).
//
// Leading-zero fidelity (V1, Daniel-fixed): the displayed/stamped string is EXACTLY
// "0.9.01" — two-digit zero-padded patch. That exactness is why the version STRING is
// the authoritative artifact (sourced verbatim from the one CMake variable), not a
// reconstruction from numeric components — CMake's `project(VERSION)` may normalize a
// numeric patch field, so we never round-trip the string through integers to render it.
#include <optional>
#include <string>
namespace reasampler {
// The canonical version string — EXACTLY the value of the CMake `REASAMPLER_VERSION`
// variable (see version_generated.h, produced by configure_file). One edit point:
// changing that variable changes this constant, the ext-state stamp, and the
// show-version action output with no other edits. Leading zero preserved verbatim.
const std::string& appVersion();
// A parsed semver triple. Kept minimal — major.minor.patch as integers, for ORDERING
// only. It deliberately does NOT round-trip back to the display string (the leading
// zero is a rendering concern owned by the authoritative string, not reconstructable
// from the integer patch). parseVersion returns nullopt on malformed input.
struct Version {
int major = 0;
int minor = 0;
int patch = 0;
};
// Parse "x.y.z" (each component a non-negative integer, leading zeros allowed) into a
// Version. Returns nullopt on anything malformed: wrong component count, non-digits, a
// leading `-`, empty components, or trailing garbage. Used for ordering two stamps and
// for validating a stored stamp before comparing.
std::optional<Version> parseVersion(const std::string& s);
// Numeric ordering by (major, minor, patch). a < b iff a precedes b. So
// 0.9.01 < 0.9.02 < 0.10.01 (numeric compare, NOT lexicographic — 10 > 9).
bool versionLess(const Version& a, const Version& b);
// The writing-version a project was last saved with, recovered from its ext-state
// stamp. A project saved before this feature shipped has NO stamp — that is the
// explicit `preVersioning` case (kind), not an error and not a warning. A present-but-
// malformed value is `unknown` (also silent — a corrupt stamp is ignored, never
// throws). A well-formed value is `stamped` and carries the exact stored string plus
// its parsed triple for comparison.
struct WritingVersion {
enum class Kind {
PreVersioning, // no stamp stored — a project written before versioning shipped
Unknown, // a stamp was stored but is not parseable — ignored, not an error
Stamped, // a well-formed stamp
};
Kind kind = Kind::PreVersioning;
std::string raw; // the exact stored string (empty for PreVersioning)
Version parsed; // meaningful only when kind == Stamped
};
// Classify a raw stored stamp value (exactly what GetProjExtState returned for the
// version key). Empty -> PreVersioning; non-empty but unparseable -> Unknown; parseable
// -> Stamped. Pure so the three-way classification is test-pinned; persist calls this
// with the raw ext-state read and never has to reason about the cases itself.
WritingVersion classifyWritingVersion(const std::string& rawStamp);
} // namespace reasampler
+31
View File
@@ -24,6 +24,7 @@
#include <vector> #include <vector>
#include "actions.h" #include "actions.h"
#include "app_version.h"
#include "bank_model.h" #include "bank_model.h"
#include "bank_panel.h" #include "bank_panel.h"
#include "capture.h" #include "capture.h"
@@ -109,6 +110,13 @@ static int g_cmdCaptureTrackRealtime = 0;
// the transport-stop. No-op (with a note) when nothing is in flight. // the transport-stop. No-op (with a note) when nothing is in flight.
static int g_cmdCancelRealtime = 0; static int g_cmdCancelRealtime = 0;
// Command id for the Phase V "show version" action. FOREVER-STABLE string. On demand
// ONLY — prints the CMake-sourced version string to the console when fired. This is the
// SOLE new console output the versioning wave adds; there is no unconditional startup
// version print (routine console chatter was deliberately removed — it pops the console
// window). The user copies this line into a bug report.
static int g_cmdShowVersion = 0;
// The persistence session (M4): owns the in-memory BankIndex and bridges it to // The persistence session (M4): owns the in-memory BankIndex and bridges it to
// project ext state. A timer tick drives g_session.poll() to detect project // project ext state. A timer tick drives g_session.poll() to detect project
// load / Save-As; capture adds Samples to g_session.bank() — which (B2) resolves to // load / Save-As; capture adds Samples to g_session.bank() — which (B2) resolves to
@@ -771,6 +779,12 @@ static bool OnHookCommand(int command, int /*flag*/)
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; } if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; }
if (command == g_cmdCaptureTrackRealtime) { RunCaptureRealtimeTrack(); return true; } if (command == g_cmdCaptureTrackRealtime) { RunCaptureRealtimeTrack(); return true; }
if (command == g_cmdCancelRealtime) { RunCancelRealtime(); return true; } if (command == g_cmdCancelRealtime) { RunCancelRealtime(); return true; }
if (command == g_cmdShowVersion)
{
// On-demand version readout — the ONLY version output on any path.
ShowConsoleMsg(("ReaSampler " + reasampler::appVersion() + "\n").c_str());
return true;
}
// Design View action family (D4). Claims only its own ids; returns false for the // Design View action family (D4). Claims only its own ids; returns false for the
// rest so this hook keeps looking (per the contract). // rest so this hook keeps looking (per the contract).
if (reasampler::designViewHandleCommand(command)) return true; if (reasampler::designViewHandleCommand(command)) return true;
@@ -795,6 +809,7 @@ static gaccel_register_t g_accelInsertSelected{};
static gaccel_register_t g_accelInsertSelectedConform{}; static gaccel_register_t g_accelInsertSelectedConform{};
static gaccel_register_t g_accelCaptureTrackRealtime{}; static gaccel_register_t g_accelCaptureTrackRealtime{};
static gaccel_register_t g_accelCancelRealtime{}; static gaccel_register_t g_accelCancelRealtime{};
static gaccel_register_t g_accelShowVersion{};
extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT( extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t* rec) REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t* rec)
@@ -826,6 +841,9 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
reasampler::designViewUnregisterActions(g_rec); reasampler::designViewUnregisterActions(g_rec);
// Tear down the multi-bank action family (B3) — same mirror-unregister. // Tear down the multi-bank action family (B3) — same mirror-unregister.
reasampler::bankUnregisterActions(g_rec); reasampler::bankUnregisterActions(g_rec);
g_rec->Register("-gaccel", (void*)&g_accelShowVersion);
g_rec->Register("-command_id",
(void*)(REASAMPLER_ACTION_PREFIX "SHOW_VERSION"));
g_rec->Register("-gaccel", (void*)&g_accelCancelRealtime); g_rec->Register("-gaccel", (void*)&g_accelCancelRealtime);
g_rec->Register("-command_id", g_rec->Register("-command_id",
(void*)(REASAMPLER_ACTION_PREFIX "CANCEL_REALTIME_CAPTURE")); (void*)(REASAMPLER_ACTION_PREFIX "CANCEL_REALTIME_CAPTURE"));
@@ -969,6 +987,19 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
rec->Register("gaccel", (void*)&g_accelCancelRealtime); rec->Register("gaccel", (void*)&g_accelCancelRealtime);
} }
// Register the Phase V "show version" action (command_id -> gaccel -> hookcommand).
// On-demand only — prints the CMake-sourced version to the console when fired; no
// startup print. FOREVER-STABLE id string.
g_cmdShowVersion = rec->Register(
"command_id",
(void*)(REASAMPLER_ACTION_PREFIX "SHOW_VERSION"));
if (g_cmdShowVersion)
{
g_accelShowVersion.accel.cmd = g_cmdShowVersion;
g_accelShowVersion.desc = "ReaSampler: show version";
rec->Register("gaccel", (void*)&g_accelShowVersion);
}
// Register the Design View action family (D4): toggle/activate mode, tag/untag/ // Register the Design View action family (D4): toggle/activate mode, tag/untag/
// show-both selected tracks. Each mints its own command_id + gaccel; the single // show-both selected tracks. Each mints its own command_id + gaccel; the single
// hookcommand below routes them via designViewHandleCommand. Registered before // hookcommand below routes them via designViewHandleCommand. Registered before
+18
View File
@@ -79,6 +79,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "app_version.h"
#include "capture_paths.h" #include "capture_paths.h"
#define REAPERAPI_MINIMAL #define REAPERAPI_MINIMAL
@@ -226,6 +227,13 @@ bool ReaSamplerSession::saveToActiveProject() {
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace, SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
kProjExtOwnedKey, ownedJson.c_str()); kProjExtOwnedKey, ownedJson.c_str());
// Phase V (V1): stamp the WRITING version — the build producing this save — under the
// version key, on the SAME seam as the keys above so the stamp and MarkProjectDirty
// stay paired (no drifting ad-hoc SetProjExtState). appVersion() is the one CMake-
// sourced constant; every save records the exact current build into the .rpp.
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
kProjExtVersionKey, appVersion().c_str());
MarkProjectDirty(static_cast<ReaProject*>(proj)); MarkProjectDirty(static_cast<ReaProject*>(proj));
return true; return true;
} }
@@ -315,6 +323,16 @@ void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDi
// (R-B) must re-read the restored manifest so it matches the rolled-back bank state. // (R-B) must re-read the restored manifest so it matches the rolled-back bank state.
owned_ = loadOwnedManifest(static_cast<ReaProject*>(proj)); owned_ = loadOwnedManifest(static_cast<ReaProject*>(proj));
// Phase V (V1): recover the writing-version stamp on EVERY load path (peer-symmetry
// with tail_/view_ above). An absent stamp classifies as PreVersioning, a malformed
// one as Unknown — both silent, no console warning (a pre-versioning project is not
// an error). getProjExtStateString returns "" for an absent key, which is exactly the
// PreVersioning input classifyWritingVersion expects. proj == nullptr -> "" -> default.
writingVersion_ = classifyWritingVersion(
proj ? getProjExtStateString(static_cast<ReaProject*>(proj), kProjExtNamespace,
kProjExtVersionKey)
: std::string{});
if (!proj) { if (!proj) {
book_ = BankBook{}; book_ = BankBook{};
return; return;
+24
View File
@@ -19,6 +19,7 @@
#include <string> #include <string>
#include "app_version.h"
#include "bank_book.h" #include "bank_book.h"
#include "bank_model.h" #include "bank_model.h"
#include "owned_manifest.h" #include "owned_manifest.h"
@@ -70,6 +71,14 @@ inline constexpr const char* kProjExtTailKey = "tail_setting";
// graceful, but the attribution safety net is lost until the next capture rebuilds it). // graceful, but the attribution safety net is lost until the next capture rebuilds it).
inline constexpr const char* kProjExtOwnedKey = "owned_files"; inline constexpr const char* kProjExtOwnedKey = "owned_files";
// The ext-state key holding the ReaSampler version that last WROTE this project
// (Phase V, V1). Written on every save alongside the banks/view/tail keys, so every
// saved .rpp records which build produced its state — the seam a future within-channel
// forward migration keys off ("this was written by 0.9.01, I am 0.9.05"). An absent
// key is the explicit pre-versioning case (a project saved before this shipped), read
// silently, never an error. FOREVER-STABLE key string once shipped.
inline constexpr const char* kProjExtVersionKey = "version";
// The ext-state key holding a GUID we mint per project to establish CONTENT-BASED // The ext-state key holding a GUID we mint per project to establish CONTENT-BASED
// project identity (REAPER exposes no stable per-project GUID). poll() uses it to // project identity (REAPER exposes no stable per-project GUID). poll() uses it to
// tell a genuine Save-As (same GUID, new .rpp path) apart from a project switch // tell a genuine Save-As (same GUID, new .rpp path) apart from a project switch
@@ -143,6 +152,15 @@ public:
OwnedFileManifest& owned() { return owned_; } OwnedFileManifest& owned() { return owned_; }
const OwnedFileManifest& owned() const { return owned_; } const OwnedFileManifest& owned() const { return owned_; }
// The ReaSampler version that last WROTE the active project, recovered from its
// ext-state stamp on load (Phase V, V1). PreVersioning when the project carries no
// stamp (saved before this feature), Unknown for a malformed stamp, Stamped with the
// exact stored string otherwise — all silent, never an error. Replaced on every load
// path (peer-symmetry with bank_/view_/tail_); default PreVersioning for an unsaved
// or never-loaded session. Exposed so a future migration step (or diagnostics) can
// reason about the origin build without re-reading ext state.
const WritingVersion& writingVersion() const { return writingVersion_; }
// Serialize the current book (under the `banks` key), view model, and tail setting // Serialize the current book (under the `banks` key), view model, and tail setting
// to the active project's ext state (namespace "reasampler"), and clear the retired // to the active project's ext state (namespace "reasampler"), and clear the retired
// legacy `bank_index` key. Non-destructive beyond writing our own ext-state keys. // legacy `bank_index` key. Non-destructive beyond writing our own ext-state keys.
@@ -208,6 +226,12 @@ private:
// the in-memory set matches disk. Absent key -> empty is graceful (older project). // the in-memory set matches disk. Absent key -> empty is graceful (older project).
OwnedFileManifest owned_; OwnedFileManifest owned_;
// The writing-version stamp recovered on load (Phase V). Default PreVersioning;
// loadFromProject replaces it on every load path (peer to bank_/view_/tail_), so
// switching to a pre-versioning project reports PreVersioning rather than inheriting
// the previous project's stamp. Read-only to consumers via writingVersion().
WritingVersion writingVersion_;
// The project identity last observed by poll(), used to detect load/Save-As. // The project identity last observed by poll(), used to detect load/Save-As.
// The GUID is the PRIMARY signal (a different stored GUID = a different project // The GUID is the PRIMARY signal (a different stored GUID = a different project
// of record = Load, immune to pointer recycling). The pointer disambiguates the // of record = Load, immune to pointer recycling). The pointer disambiguates the
+9
View File
@@ -0,0 +1,9 @@
#pragma once
// version_generated.h.in — configure_file TEMPLATE. CMake substitutes @REASAMPLER_VERSION@
// (the ONE source-of-truth variable in CMakeLists.txt) and writes the result to the build
// tree as version_generated.h. DO NOT edit the generated header — edit REASAMPLER_VERSION
// in CMakeLists.txt; that single edit re-generates this and re-threads the exact string
// (leading zero preserved verbatim) into the binary constant, the ext-state stamp, and the
// show-version action. See app_version.h / .cpp.
#define REASAMPLER_VERSION_STRING "@REASAMPLER_VERSION@"
+138
View File
@@ -0,0 +1,138 @@
// 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/app_version.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)
// --- appVersion: exact-string fidelity, leading zero preserved ----------------
static void testVersionConstantRendersExactString() {
// The Daniel-fixed string: EXACTLY "0.9.01", two-digit zero-padded patch. This is
// the whole point of sourcing the STRING (not reconstructing from numeric components):
// a normalized "0.9.1" here would be a leading-zero-fidelity regression, and this
// assertion fails against the actual CMake-configured value, not a re-derivation.
CHECK(appVersion() == "0.9.01");
}
// --- 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() {
testVersionConstantRendersExactString();
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;
}