From 379067709777890aad35c35f520396b67a2a3ff6 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 27 Jul 2026 22:51:16 -0400 Subject: [PATCH] bump to 0.9.8; add padded-canary build (app_version_padding_tests) Canary re-proven on this base: reconstruct-from-components trips the fixture checks; an unpadded fixture trips the non-vacuity assertion. De-literalizes stale version examples in comments. --- CMakeLists.txt | 76 ++++++++++++++++++++++++--- src/app_version.h | 35 ++++++++----- src/bank_panel.cpp | 7 +-- src/vst/vst_entry.cpp | 5 +- tests/test_app_version_padding.cpp | 84 ++++++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 27 deletions(-) create mode 100644 tests/test_app_version_padding.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e87bcb4..163807f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,15 +4,34 @@ cmake_minimum_required(VERSION 3.19) # 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") +# authoritative verbatim — any leading zeros are preserved exactly as written (Daniel's +# ruling: padded and unpadded versions are BOTH legitimate — "0.9.8" and "0.9.80" are +# different versions, and the system must never force zero-padding). 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 for CMake hygiene / any downstream numeric use, but +# it is NOT the rendered source of truth. +# +# *** INVARIANT — DO NOT COLLAPSE OR DERIVE *** +# The set(REASAMPLER_VERSION ...) line below MUST remain a verbatim string literal, +# even when it is textually identical to the project(VERSION ...) line. It must NEVER +# become +# set(REASAMPLER_VERSION "${PROJECT_VERSION}") +# set(REASAMPLER_VERSION "${CMAKE_PROJECT_VERSION}") +# or any other form that derives the string from project(). CMake normalizes numeric +# patch fields: a padded "0.9.01" would silently round-trip to "0.9.1" and break the +# displayed version string. The verbatim-threading invariant is guarded at build time +# by app_version_padding_tests (a synthetic zero-padded canary; see the padding-canary +# block after app_version_tests). That guard detects reconstruct-from-components +# regressions in app_version.cpp but does NOT guard against this line being changed to +# a CMake variable derivation — that is this comment's job. +# +# There is a SECOND set(REASAMPLER_VERSION ...) inside the padding-canary function +# further down. That one is a permanent test fixture ("0.9.01") and must NEVER be +# bumped on release. +set(REASAMPLER_VERSION "0.9.8") -project(reaper_reasampler VERSION 0.9.1 LANGUAGES CXX) +project(reaper_reasampler VERSION 0.9.8 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -584,6 +603,47 @@ 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) +# Anti-normalization padding canary (see tests/test_app_version_padding.cpp for the full +# rationale). The live-version assertions in app_version_tests can only DETECT a +# "reconstruct the string from numeric components" regression when the live version +# happens to be zero-padded — at an unpadded version (e.g. 0.9.8) the reconstruction +# coincides with the verbatim string and the check silently goes vacuous. So: run the +# SAME version_generated.h.in template through configure_file a second time with a +# SYNTHETIC padded version, and compile the SAME src/app_version.cpp against that header +# (include-dir substitution — the canary target never sees the live generated/ dir). +# The "0.9.01" here is a permanent test fixture, NOT the shipped version (see also the +# source-of-truth comment at the top of this file for the live-version invariant); it +# must match the literals in test_app_version_padding.cpp and must never be bumped on +# release. +# +# COVERAGE BOUNDARY: the canary detects "reconstruct-from-numeric-components" +# regressions inside app_version.cpp. It does NOT detect the live source-of-truth line +# becoming a CMake variable derivation; that case is separately guarded by the comment +# block at the top of this file. +function(_configure_padding_canary) + # set() inside a function is function-scoped — the clobber cannot leak into the + # parent scope, so no save/restore/unset dance is needed. REASAMPLER_CHANNEL_IS_BETA + # and CMAKE_CURRENT_SOURCE_DIR / CMAKE_CURRENT_BINARY_DIR are inherited read-only. + set(REASAMPLER_VERSION "0.9.01") + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/src/version_generated.h.in + ${CMAKE_CURRENT_BINARY_DIR}/generated_padding_canary/version_generated.h + @ONLY) +endfunction() +_configure_padding_canary() + +# NOTE: app_version_padding_tests deliberately recompiles src/app_version.cpp rather +# than linking the app_version library target. This is required for the include-dir +# substitution to work — the canary needs to see generated_padding_canary/ instead of +# the live generated/ dir. If app_version ever gains a link dependency (e.g. a new +# pure-module link), the canary target_link_libraries must mirror it here. +add_executable(app_version_padding_tests + tests/test_app_version_padding.cpp + src/app_version.cpp) +target_include_directories(app_version_padding_tests PRIVATE + ${CMAKE_CURRENT_BINARY_DIR}/generated_padding_canary src) +add_test(NAME app_version_padding_tests COMMAND app_version_padding_tests) + # The provenance test links bank_model too — it proves the recorded recipe survives # the Sample-JSON round-trip (Provenance.fxChainSnapshot), the M1 seam M10 rides on. add_executable(provenance_tests tests/test_provenance.cpp) diff --git a/src/app_version.h b/src/app_version.h index 3bee96f..f940e6f 100644 --- a/src/app_version.h +++ b/src/app_version.h @@ -21,11 +21,17 @@ // 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 +// Leading-zero fidelity (V1, Daniel-fixed): the displayed/stamped string preserves the +// configured version EXACTLY as written — padded and unpadded versions are both +// legitimate (e.g. "0.9.8" and "0.9.80" are different versions; a "0.9.01" renders its +// zero-padded patch verbatim). 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. +// Guarded against reconstruct-from-components regressions in app_version.cpp by +// app_version_padding_tests (the padded canary build). The canary does NOT guard +// against the CMakeLists.txt source-of-truth line being changed to a CMake variable +// derivation — that case is guarded by the comment block at the top of CMakeLists.txt. #include #include @@ -48,20 +54,21 @@ Channel channel(); // True on the beta build only. Convenience over channel() == Channel::Beta. bool isBeta(); -// The user-visible version render. Stable: EXACTLY the CMake string ("0.9.01"). Beta: -// that string plus a plain "-beta" suffix ("0.9.01-beta") — a plain suffix, NOT a -// git-describe decoration (V2, Daniel-fixed). This is what the show-version action and -// the bank-panel readout display. It is NOT the ext-state stamp value (see stampVersion). +// The user-visible version render. Stable: EXACTLY the configured CMake string. Beta: +// that string plus a plain "-beta" suffix — a plain suffix, NOT a git-describe +// decoration (V2, Daniel-fixed). This is what the show-version action and the +// bank-panel readout display. It is NOT the ext-state stamp value (see stampVersion). const std::string& appVersion(); // The ext-state STAMP value — the writing-version recorded into a saved project. This is -// the NUMERIC TRIPLE ONLY ("0.9.01") on BOTH channels: it deliberately carries NO channel -// suffix, so (a) parseVersion classifies it as Stamped when its own channel reads it back -// (a "-beta"-suffixed stamp would classify as Unknown — the V4 stamp-classifiability -// requirement), and (b) stable's stamp value is byte-identical regardless of the channel -// build. The channel is carried by the ISOLATED namespace (see extStateNamespace), never -// baked into the stamp. Distinct from appVersion() precisely so the display can say -// "-beta" while the stamp stays classifiable and stable-identical. +// the NUMERIC TRIPLE ONLY (the configured string, no channel suffix) on BOTH channels: +// it deliberately carries NO channel suffix, so (a) parseVersion classifies it as +// Stamped when its own channel reads it back (a "-beta"-suffixed stamp would classify +// as Unknown — the V4 stamp-classifiability requirement), and (b) stable's stamp value +// is byte-identical regardless of the channel build. The channel is carried by the +// ISOLATED namespace (see extStateNamespace), never baked into the stamp. Distinct from +// appVersion() precisely so the display can say "-beta" while the stamp stays +// classifiable and stable-identical. const std::string& stampVersion(); // The project ext-state namespace this channel reads and writes. Stable: "reasampler" diff --git a/src/bank_panel.cpp b/src/bank_panel.cpp index ef5227b..64a64c7 100644 --- a/src/bank_panel.cpp +++ b/src/bank_panel.cpp @@ -740,9 +740,10 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) { } // Version/channel readout (Phase V, V3/V4), right-aligned, unobtrusive. appVersion() - // renders "0.9.01" on stable and "0.9.01-beta" on beta so a beta panel self-identifies. - // It sits inside the space footer_bar reserves at the right (rightReserve) and clears the - // prune button (prune_button::rightInset). Dim, passive identification (V3). + // renders the configured version string on stable and that string plus "-beta" on beta, + // so a beta panel self-identifies. It sits inside the space footer_bar reserves at the + // right (rightReserve) and clears the prune button (prune_button::rightInset). Dim, + // passive identification (V3). kitText(bmp, KitBox{f.left, f.top, (f.right - f.left) - 8, f.bottom - f.top}, reasampler::appVersion().c_str(), Font::Micro, Role::TextDim, Align::Right); diff --git a/src/vst/vst_entry.cpp b/src/vst/vst_entry.cpp index 66151e5..ea49a15 100644 --- a/src/vst/vst_entry.cpp +++ b/src/vst/vst_entry.cpp @@ -66,8 +66,9 @@ BEGIN_FACTORY(reasampler::vst::kVendorName, reasampler::vst::kVendorUrl, // as literals. DEF_CLASS2 expands inside GetPluginFactory() and PClassInfo2's constructor // copies the char* into its own fixed buffer at that runtime call, so .c_str() on the // accessors' static-storage strings is valid (no dangling — the refs outlive the copy). -// vstPluginName(): "ReaSampler 9000" / "ReaSampler 9000 beta". appVersion(): "0.9.01" / -// "0.9.01-beta" (the -beta render V4 already yields on beta). +// vstPluginName(): "ReaSampler 9000" / "ReaSampler 9000 beta" (live literals in +// app_version.cpp). appVersion(): the configured version string / that string plus +// "-beta" (the -beta render V4 already yields on beta). DEF_CLASS2(INLINE_UID(REASAMPLER_ACTIVE_UID_1, REASAMPLER_ACTIVE_UID_2, REASAMPLER_ACTIVE_UID_3, REASAMPLER_ACTIVE_UID_4), Steinberg::PClassInfo::kManyInstances, // cardinality diff --git a/tests/test_app_version_padding.cpp b/tests/test_app_version_padding.cpp new file mode 100644 index 0000000..1b49066 --- /dev/null +++ b/tests/test_app_version_padding.cpp @@ -0,0 +1,84 @@ +// test_app_version_padding.cpp — the ALWAYS-ON anti-normalization guard for the version +// string threading (Phase V, V1 leading-zero fidelity). No REAPER, no framework. +// +// WHY THIS EXECUTABLE EXISTS: test_app_version.cpp pins stampVersion() relative to the +// live configure_file'd version string — but a live-version assertion can only DETECT a +// "reconstruct the string from numeric components" regression when the live version +// happens to be zero-padded. "0.9.01" reconstructed via integers renders "0.9.1" and +// trips it; "0.9.8" reconstructs to itself, so at an unpadded version the check passes +// whether or not the code regressed — the guard silently goes vacuous while green. +// Padded and unpadded versions are both legitimate (Daniel's ruling), so the guard must +// not depend on the shipped version's shape. +// +// MECHANISM: CMakeLists.txt runs the SAME src/version_generated.h.in template through +// configure_file a second time with a SYNTHETIC zero-padded version ("0.9.01") into +// generated_padding_canary/, and this executable compiles the SAME src/app_version.cpp +// against that header (include-dir substitution — this target never sees the live +// generated/ dir). The exact production derivation path — template substitution → +// REASAMPLER_VERSION_STRING → stampVersion()/appVersion() — is thereby exercised with an +// input whose normalized form provably differs from its verbatim form. If anyone +// reimplements the version string by reconstructing it from numeric components (a +// parse-and-reprint inside app_version.cpp, or project(VERSION) numeric macros added to +// the template), this binary renders "0.9.1" and the checks below fail — at ANY live +// version, which is exactly the property the live-version assertion cannot provide. +// +// The "0.9.01" literals below are the synthetic fixture and MUST match the canary +// configure_file input in CMakeLists.txt. They are NOT the shipped version: never bump +// them on release — their padded shape is the entire point. + +#include "../src/app_version.h" + +#include +#include + +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) + +static void testPaddedVersionSurvivesThreadingVerbatim() { + // The verbatim-threading invariant, exercised with an input where reconstruction and + // verbatim DIFFER: a normalized render would be "0.9.1" and fail both checks. + CHECK(stampVersion() == "0.9.01"); + if (isBeta()) CHECK(appVersion() == "0.9.01-beta"); + else CHECK(appVersion() == "0.9.01"); +} + +static void testPaddedStampStillClassifiesStamped() { + // A padded stamp stays classifiable on read-back: parse tolerates leading zeros + // (raw preserved verbatim, triple normalized to integers — {0,9,1}). + WritingVersion wv = classifyWritingVersion(stampVersion()); + 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 testCanaryFixtureIsNonVacuous() { + // Self-check: the canary fixture must be zero-padded so that its verbatim string + // differs from what a parse-and-reprint of its numeric triple would produce. + // If the fixture is ever "fixed" to an unpadded form (e.g. "0.9.1"), the checks in + // testPaddedVersionSurvivesThreadingVerbatim become vacuous — a reconstructed + // string would match the fixture and the canary would pass even if the threading + // regressed. Parse stampVersion() and reprint the triple; they must NOT match. + WritingVersion wv = classifyWritingVersion(stampVersion()); + CHECK(wv.kind == WritingVersion::Kind::Stamped); // fixture must be parseable + const std::string reprinted = + std::to_string(wv.parsed.major) + "." + + std::to_string(wv.parsed.minor) + "." + + std::to_string(wv.parsed.patch); + // If this fails, the fixture is unpadded and the canary guards nothing. + // Restore a zero-padded fixture (e.g. "0.9.01") in CMakeLists.txt and + // update the matching literals in this file. + CHECK(reprinted != stampVersion()); +} + +int main() { + testPaddedVersionSurvivesThreadingVerbatim(); + testPaddedStampStillClassifiesStamped(); + testCanaryFixtureIsNonVacuous(); + + if (g_fail == 0) std::printf("app_version_padding: all tests passed\n"); + else std::printf("app_version_padding: %d CHECK(s) FAILED\n", g_fail); + return g_fail == 0 ? 0 : 1; +}