bake: name the five ways the extension can fail to answer, and move the landing verdict into a pure, tab-provable classifier
A no-answer stays a failure; it now says whether the extension never ran the landing, answered a stale generation, spoke a wire this build cannot read, cleared the request, or refused it.
This commit is contained in:
+210
-1
@@ -6,13 +6,16 @@
|
||||
// break a mixed-version pair); request + outcome round-trips including bytes that would
|
||||
// break a delimiter-based format; the refusals every house wire record shares (wrong tag,
|
||||
// truncation, trailing garbage, a swapped record kind); an unrecognized status integer
|
||||
// degrading to Failed rather than to Ok; and the action lookup name's leading underscore.
|
||||
// degrading to Failed rather than to Ok; the action lookup name's leading underscore and
|
||||
// its channel fork; and the two key classifiers each end reads the shared key through.
|
||||
|
||||
#include "../src/core/wire/bake_wire.h"
|
||||
|
||||
#include "../src/core/version/app_version.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
using namespace reasampler::wire;
|
||||
@@ -183,6 +186,212 @@ int main() {
|
||||
CHECK(lookup.compare(lookup.size() - suffixLen, suffixLen, kBakeActionSuffix) == 0);
|
||||
}
|
||||
|
||||
// --- The action id and the ext-state namespace fork on the SAME channel bit ---------
|
||||
// Both frozen families are spelled out, so this holds whichever channel the test binary
|
||||
// was built for. Consequence for a cross-channel pair (stable VST + beta extension or
|
||||
// the reverse): NamedCommandLookup resolves nothing, bakeAvailable paints the affordance
|
||||
// Disabled, and the click cannot reach the no-answer path at all.
|
||||
{
|
||||
const std::string suffix = kBakeActionSuffix;
|
||||
const std::string stableLookup = "_CEREBELLUM_REASAMPLER_" + suffix;
|
||||
const std::string betaLookup = "_CEREBELLUM_REASAMPLER_BETA_" + suffix;
|
||||
const bool beta = reasampler::version::isBeta();
|
||||
|
||||
CHECK(stableLookup != betaLookup);
|
||||
CHECK(bakeActionLookupName() == (beta ? betaLookup : stableLookup));
|
||||
CHECK(bakeActionLookupName() != (beta ? stableLookup : betaLookup));
|
||||
CHECK(reasampler::version::extStateNamespace() ==
|
||||
(beta ? "reasampler_beta" : "reasampler"));
|
||||
}
|
||||
|
||||
// --- What the instrument finds under its own key, after the action returned ---------
|
||||
{
|
||||
BakeRequest sent;
|
||||
sent.instanceGuid = "0123abcd";
|
||||
sent.stagedFilePath = "C:/Temp/reasampler_bake_0123abcd_1893456000.wav";
|
||||
sent.sourceSampleId = "cap-1";
|
||||
sent.sourceRelativePath = "reasampler_bank/kick.wav";
|
||||
sent.sourceDisplayName = "Kick";
|
||||
sent.ownUsageKey = "rsusage_0123abcd";
|
||||
sent.rootNote = 36;
|
||||
sent.generation = 1893456000;
|
||||
|
||||
BakeOutcome answered;
|
||||
answered.status = BakeStatus::Ok;
|
||||
answered.sampleId = "bake-1";
|
||||
answered.message = "added as a distinct capture";
|
||||
answered.generation = sent.generation;
|
||||
|
||||
const BakeAnswer ok =
|
||||
classifyBakeAnswer(std::optional<std::string>(encodeBakeOutcome(answered)), sent);
|
||||
CHECK(ok.kind == BakeAnswerKind::Answered);
|
||||
CHECK(ok.outcome.has_value() && ok.outcome->sampleId == "bake-1");
|
||||
|
||||
// A refusal is an ANSWER — it must never fold into a no-answer kind, or the user is
|
||||
// sent to reinstall a binary that in fact answered them.
|
||||
BakeOutcome refused;
|
||||
refused.status = BakeStatus::WrongProject;
|
||||
refused.message = "this bake's project tab is not the one the extension has loaded";
|
||||
refused.generation = sent.generation;
|
||||
const BakeAnswer refusal =
|
||||
classifyBakeAnswer(std::optional<std::string>(encodeBakeOutcome(refused)), sent);
|
||||
CHECK(refusal.kind == BakeAnswerKind::Answered);
|
||||
CHECK(refusal.outcome.has_value() &&
|
||||
refusal.outcome->status == BakeStatus::WrongProject);
|
||||
|
||||
BakeOutcome older = answered;
|
||||
older.generation = sent.generation - 1;
|
||||
const BakeAnswer foreignOut =
|
||||
classifyBakeAnswer(std::optional<std::string>(encodeBakeOutcome(older)), sent);
|
||||
CHECK(foreignOut.kind == BakeAnswerKind::ForeignOutcome);
|
||||
CHECK(foreignOut.outcome.has_value());
|
||||
|
||||
// Nothing on the extension side read the key: the request is still sitting there
|
||||
// byte-for-byte. THE diagnostic that separates a stale/absent landing from a refusal.
|
||||
CHECK(classifyBakeAnswer(std::optional<std::string>(encodeBakeRequest(sent)), sent)
|
||||
.kind == BakeAnswerKind::Unanswered);
|
||||
|
||||
// A request that is not ours: two instances copied from one another share a
|
||||
// persisted instanceGuid, so they name one key.
|
||||
BakeRequest sibling = sent;
|
||||
sibling.sourceSampleId = "cap-2";
|
||||
sibling.generation = sent.generation + 1;
|
||||
CHECK(classifyBakeAnswer(std::optional<std::string>(encodeBakeRequest(sibling)), sent)
|
||||
.kind == BakeAnswerKind::ForeignRequest);
|
||||
|
||||
CHECK(classifyBakeAnswer(std::nullopt, sent).kind == BakeAnswerKind::Cleared);
|
||||
CHECK(classifyBakeAnswer(std::optional<std::string>(""), sent).kind ==
|
||||
BakeAnswerKind::Cleared);
|
||||
CHECK(classifyBakeAnswer(std::optional<std::string>("rsbakeout9 whatever"), sent)
|
||||
.kind == BakeAnswerKind::Undecodable);
|
||||
|
||||
// No non-Answered kind may carry an outcome the caller could read as a landing.
|
||||
CHECK(!classifyBakeAnswer(std::nullopt, sent).outcome.has_value());
|
||||
CHECK(!classifyBakeAnswer(std::optional<std::string>(encodeBakeRequest(sent)), sent)
|
||||
.outcome.has_value());
|
||||
CHECK(!classifyBakeAnswer(std::optional<std::string>("garbage"), sent)
|
||||
.outcome.has_value());
|
||||
}
|
||||
|
||||
// --- The extension's per-key verdict over the open tabs -----------------------------
|
||||
{
|
||||
const std::int64_t now = 1893456000;
|
||||
// The session has polled a project and that project is REAPER's active tab — the
|
||||
// steady state a project opened from disk reaches on the next timer tick.
|
||||
const BakeScanContext loadedAndActive{true, true};
|
||||
|
||||
BakeScanKey own{true, now, true};
|
||||
CHECK(classifyBakeScan(loadedAndActive, own, now) == BakeScanVerdict::Land);
|
||||
|
||||
// A request found in a tab the extension has NOT loaded — the multi-tab case. It is
|
||||
// REFUSED, which is an answer the asking instance can read; it is never landed into
|
||||
// the loaded tab's bank, and never dropped silently.
|
||||
BakeScanKey otherTab{true, now, false};
|
||||
CHECK(classifyBakeScan(loadedAndActive, otherTab, now) ==
|
||||
BakeScanVerdict::RefuseWrongProject);
|
||||
|
||||
// The loaded tab is no longer the active one, so a persist would write elsewhere:
|
||||
// even the loaded tab's own request is refused rather than half-landed.
|
||||
CHECK(classifyBakeScan(BakeScanContext{true, false}, own, now) ==
|
||||
BakeScanVerdict::RefuseWrongProject);
|
||||
// The window between opening a project from disk and the first poll: no book is
|
||||
// loaded yet, so nothing may land.
|
||||
CHECK(classifyBakeScan(BakeScanContext{false, false}, own, now) ==
|
||||
BakeScanVerdict::RefuseWrongProject);
|
||||
|
||||
// Stale in EITHER direction (a clock that moved backwards counts too).
|
||||
BakeScanKey old{true, now - kMaxRequestAgeSeconds - 1, true};
|
||||
BakeScanKey future{true, now + kMaxRequestAgeSeconds + 1, true};
|
||||
CHECK(classifyBakeScan(loadedAndActive, old, now) == BakeScanVerdict::ClearStale);
|
||||
CHECK(classifyBakeScan(loadedAndActive, future, now) == BakeScanVerdict::ClearStale);
|
||||
// Exactly at the bound is still landable — the ceiling is inclusive.
|
||||
BakeScanKey atBound{true, now - kMaxRequestAgeSeconds, true};
|
||||
CHECK(classifyBakeScan(loadedAndActive, atBound, now) == BakeScanVerdict::Land);
|
||||
// Staleness outranks the project check: a request nobody can read is cleared, not
|
||||
// answered, wherever it sits.
|
||||
BakeScanKey oldElsewhere{true, now - kMaxRequestAgeSeconds - 1, false};
|
||||
CHECK(classifyBakeScan(loadedAndActive, oldElsewhere, now) ==
|
||||
BakeScanVerdict::ClearStale);
|
||||
|
||||
// A value that is not a request (an outcome the writer has not collected) is left
|
||||
// alone — answering it would clobber an answer in flight.
|
||||
BakeScanKey notARequest{false, 0, true};
|
||||
CHECK(classifyBakeScan(loadedAndActive, notARequest, now) ==
|
||||
BakeScanVerdict::Ignore);
|
||||
CHECK(classifyBakeScan(BakeScanContext{false, false}, notARequest, now) ==
|
||||
BakeScanVerdict::Ignore);
|
||||
}
|
||||
|
||||
// --- Every outcome bake_land actually emits survives the key round trip -------------
|
||||
// The landing writes these and the instrument reads them back; a field the encoder and
|
||||
// the decoder disagreed about would strand exactly the bake that produced it.
|
||||
{
|
||||
BakeOutcome added;
|
||||
added.status = BakeStatus::Ok;
|
||||
added.sampleId = "bake-1893456000-a1b2c3d4-kick_1893456000.wav";
|
||||
added.relativePath = "reasampler_bank/kick_1893456000-a1b2c3d4.wav";
|
||||
added.displayName = "Kick 2";
|
||||
added.rootNote = 36;
|
||||
added.channelCount = 2;
|
||||
added.replaced = false;
|
||||
added.message = "added as a distinct capture";
|
||||
added.generation = 1893456000;
|
||||
|
||||
BakeOutcome replaced = added;
|
||||
replaced.replaced = true;
|
||||
replaced.displayName = "Kick";
|
||||
replaced.message = "replaced the bank entry";
|
||||
|
||||
BakeOutcome deduped = added;
|
||||
deduped.message = "identical to an existing capture -- pointed at it";
|
||||
|
||||
BakeOutcome noProject;
|
||||
noProject.status = BakeStatus::NoProject;
|
||||
noProject.message = "no saved project, so the bank has no location";
|
||||
noProject.generation = added.generation;
|
||||
|
||||
BakeOutcome stagedMissing = noProject;
|
||||
stagedMissing.status = BakeStatus::StagedMissing;
|
||||
stagedMissing.message = "the staged render is not a usable WAV";
|
||||
|
||||
BakeOutcome noSource = noProject;
|
||||
noSource.status = BakeStatus::NoSource;
|
||||
noSource.message = "the resampled capture is not in any bank";
|
||||
|
||||
BakeOutcome indexRejected = noProject;
|
||||
indexRejected.status = BakeStatus::IndexRejected;
|
||||
indexRejected.message = "the bank refused the new capture";
|
||||
|
||||
BakeOutcome wrongProject = noProject;
|
||||
wrongProject.status = BakeStatus::WrongProject;
|
||||
wrongProject.message =
|
||||
"this bake's project tab is not the one the extension has "
|
||||
"loaded -- focus that tab and try again";
|
||||
|
||||
BakeOutcome writeFailed = noProject;
|
||||
writeFailed.status = BakeStatus::Failed;
|
||||
writeFailed.message = "could not write the bake into the bank folder";
|
||||
|
||||
for (const BakeOutcome& emitted :
|
||||
{added, replaced, deduped, noProject, stagedMissing, noSource, indexRejected,
|
||||
wrongProject, writeFailed}) {
|
||||
const auto back = decodeBakeOutcome(encodeBakeOutcome(emitted));
|
||||
CHECK(back.has_value());
|
||||
CHECK(back.has_value() && *back == emitted);
|
||||
// Field for field as well: operator== is hand-written, so a field missing from
|
||||
// BOTH the codec and the comparison would pass the aggregate check above.
|
||||
CHECK(back.has_value() && back->status == emitted.status &&
|
||||
back->sampleId == emitted.sampleId &&
|
||||
back->relativePath == emitted.relativePath &&
|
||||
back->displayName == emitted.displayName &&
|
||||
back->rootNote == emitted.rootNote &&
|
||||
back->channelCount == emitted.channelCount &&
|
||||
back->replaced == emitted.replaced &&
|
||||
back->message == emitted.message &&
|
||||
back->generation == emitted.generation);
|
||||
}
|
||||
}
|
||||
|
||||
if (g_fail == 0) std::printf("bake_wire: all tests passed\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user