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:
+40
-2
@@ -1,10 +1,30 @@
|
||||
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_REQUIRED 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):
|
||||
# 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_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).
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -267,6 +300,10 @@ add_executable(owned_manifest_tests tests/test_owned_manifest.cpp)
|
||||
target_link_libraries(owned_manifest_tests PRIVATE owned_manifest)
|
||||
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).
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -303,8 +340,9 @@ add_library(reaper_reasampler MODULE
|
||||
src/actions.cpp
|
||||
src/bank_book.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})
|
||||
set_target_properties(reaper_reasampler PROPERTIES PREFIX "" OUTPUT_NAME "reaper_reasampler")
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "actions.h"
|
||||
#include "app_version.h"
|
||||
#include "bank_model.h"
|
||||
#include "bank_panel.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.
|
||||
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
|
||||
// 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
|
||||
@@ -771,6 +779,12 @@ static bool OnHookCommand(int command, int /*flag*/)
|
||||
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; }
|
||||
if (command == g_cmdCaptureTrackRealtime) { RunCaptureRealtimeTrack(); 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
|
||||
// rest so this hook keeps looking (per the contract).
|
||||
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_accelCaptureTrackRealtime{};
|
||||
static gaccel_register_t g_accelCancelRealtime{};
|
||||
static gaccel_register_t g_accelShowVersion{};
|
||||
|
||||
extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
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);
|
||||
// Tear down the multi-bank action family (B3) — same mirror-unregister.
|
||||
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("-command_id",
|
||||
(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);
|
||||
}
|
||||
|
||||
// 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/
|
||||
// show-both selected tracks. Each mints its own command_id + gaccel; the single
|
||||
// hookcommand below routes them via designViewHandleCommand. Registered before
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "app_version.h"
|
||||
#include "capture_paths.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
@@ -226,6 +227,13 @@ bool ReaSamplerSession::saveToActiveProject() {
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
|
||||
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));
|
||||
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.
|
||||
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) {
|
||||
book_ = BankBook{};
|
||||
return;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "app_version.h"
|
||||
#include "bank_book.h"
|
||||
#include "bank_model.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).
|
||||
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
|
||||
// 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
|
||||
@@ -143,6 +152,15 @@ public:
|
||||
OwnedFileManifest& owned() { 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
|
||||
// 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.
|
||||
@@ -208,6 +226,12 @@ private:
|
||||
// the in-memory set matches disk. Absent key -> empty is graceful (older project).
|
||||
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 GUID is the PRIMARY signal (a different stored GUID = a different project
|
||||
// of record = Load, immune to pointer recycling). The pointer disambiguates the
|
||||
|
||||
@@ -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@"
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user