Files
reasampler/tests/test_bake_wire.cpp
T
daniel 6cabbd0dd4 Fix the bake scan report's unsound absence-as-signal claim
Gate the summary on any unanswered key and on SetProjExtState's return, print one line per scanned key, and make the undo block stack RAII.
2026-08-02 08:34:59 -04:00

649 lines
31 KiB
C++

// Standalone tests for reasampler::wire::bake_wire — no VST3, no REAPER, no framework.
// Same fast assert loop as the sibling wire tests.
//
// Covers: the exact bytes each record encodes to (the two artifacts ship independently, so
// a field reorder or an inserted field must fail here rather than pass a round-trip and
// 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; 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;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
int main() {
// --- The exact bytes on the wire -------------------------------------------------
// A round-trip alone would pass a reordered or inserted field; the tags exist to guard
// the LAYOUT, so the layout is what is pinned. Changing either literal below means an
// already-shipped pair of artifacts can no longer talk — bump the tag, don't edit it.
{
BakeRequest req;
req.instanceGuid = "abcd";
req.stagedFilePath = "T/b.wav";
req.sourceSampleId = "cap-1";
req.sourceRelativePath = "bank/k.wav";
req.sourceDisplayName = "Kick";
req.ownUsageKey = "rsusage_abcd";
req.rootNote = 36;
req.generation = 1893456000;
CHECK(encodeBakeRequest(req) ==
"rsbakereq1"
"4:abcd"
"7:T/b.wav"
"5:cap-1"
"10:bank/k.wav"
"4:Kick"
"12:rsusage_abcd"
"2:36"
"10:1893456000");
BakeOutcome out;
out.status = BakeStatus::Ok;
out.sampleId = "bake-1";
out.relativePath = "bank/k2.wav";
out.displayName = "Kick r2";
out.rootNote = 36;
out.channelCount = 2;
out.replaced = true;
out.message = "replaced";
out.generation = 1893456000;
CHECK(encodeBakeOutcome(out) ==
"rsbakeout1"
"1:0"
"6:bake-1"
"11:bank/k2.wav"
"7:Kick r2"
"2:36"
"1:2"
"1:1"
"8:replaced"
"10:1893456000");
}
// --- Request round-trip, with hostile field content -----------------------------
{
BakeRequest req;
req.instanceGuid = "0123abcd";
req.stagedFilePath = "C:/Temp/re:sampler 9000/bake 12:34.wav"; // colons + spaces
req.sourceSampleId = "cap-1";
req.sourceRelativePath = "reasampler_bank/kick.wav";
req.sourceDisplayName = "Kick r2";
req.ownUsageKey = "rsusage_0123abcd";
req.rootNote = 36;
req.generation = 1893456000;
const std::string encoded = encodeBakeRequest(req);
const auto decoded = decodeBakeRequest(encoded);
CHECK(decoded.has_value());
CHECK(*decoded == req);
// Empty strings and a zero generation survive too (a first, un-named source).
BakeRequest bare;
CHECK(decodeBakeRequest(encodeBakeRequest(bare)) == bare);
}
// --- Outcome round-trip -----------------------------------------------------------
{
BakeOutcome out;
out.status = BakeStatus::Ok;
out.sampleId = "bake-1893456000-kick_1893456000.wav";
out.relativePath = "reasampler_bank/kick_1893456000.wav";
out.displayName = "Kick r3";
out.rootNote = 36;
out.channelCount = 2;
out.replaced = true;
out.message = "replaced the bank entry";
out.generation = 1893456000;
const auto decoded = decodeBakeOutcome(encodeBakeOutcome(out));
CHECK(decoded.has_value());
CHECK(*decoded == out);
CHECK(decoded->replaced);
out.replaced = false;
CHECK(decodeBakeOutcome(encodeBakeOutcome(out))->replaced == false);
}
// --- Malformed input is refused, never half-parsed ---------------------------------
{
BakeRequest req;
req.instanceGuid = "abc";
req.rootNote = 60;
const std::string good = encodeBakeRequest(req);
CHECK(!decodeBakeRequest("").has_value());
CHECK(!decodeBakeRequest("rsbakereq0" + good.substr(10)).has_value()); // wrong tag
CHECK(!decodeBakeRequest(good.substr(0, good.size() - 3)).has_value()); // truncated
CHECK(!decodeBakeRequest(good + "junk").has_value()); // trailing
// The two records share a key; each must refuse the other's bytes outright.
CHECK(!decodeBakeOutcome(good).has_value());
CHECK(!decodeBakeRequest(encodeBakeOutcome(BakeOutcome{})).has_value());
}
// --- A status integer this build does not know reads as a FAILURE ------------------
{
// Hand-built with a future status value; every other field is well-formed, so only
// the vocabulary gap is under test.
BakeOutcome out;
out.status = BakeStatus::Ok;
out.generation = 7;
std::string wire = encodeBakeOutcome(out);
// The status field is the first after the tag: "<len>':'<digits>".
const std::string okField = "1:0";
const std::size_t at = wire.find(okField);
CHECK(at != std::string::npos);
wire.replace(at, okField.size(), "2:99");
const auto decoded = decodeBakeOutcome(wire);
CHECK(decoded.has_value());
CHECK(decoded->status == BakeStatus::Failed); // never Ok
CHECK(decoded->generation == 7);
// Every status this build DOES know survives its own round trip — including the
// most recently appended one, which an older reader will see as Failed.
for (const BakeStatus s :
{BakeStatus::Ok, BakeStatus::Failed, BakeStatus::NoProject,
BakeStatus::StagedMissing, BakeStatus::NoSource, BakeStatus::IndexRejected,
BakeStatus::WrongProject}) {
BakeOutcome one;
one.status = s;
const auto back = decodeBakeOutcome(encodeBakeOutcome(one));
CHECK(back.has_value() && back->status == s);
}
}
// --- The lookup name carries the underscore the registration string does not -------
// The load-bearing half is the REGISTRATION string: main.cpp registers that spelling
// verbatim, and NamedCommandLookup needs exactly one underscore in front of it. If
// channelCommandId ever grew one of its own, the lookup would carry two and resolve to
// nothing.
{
const std::string registered =
reasampler::version::channelCommandId(kBakeActionSuffix);
const std::string lookup = bakeActionLookupName();
const std::size_t suffixLen = std::string(kBakeActionSuffix).size();
CHECK(!registered.empty());
CHECK(registered.front() != '_');
CHECK(lookup.size() == registered.size() + 1);
CHECK(lookup.front() == '_' && lookup[1] != '_');
CHECK(lookup.compare(1, std::string::npos, registered) == 0);
// The suffix is the TAIL of the id — the channel prefix goes in front of it, and a
// suffix that drifted into the middle would name a different action.
CHECK(lookup.size() > suffixLen);
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);
// Cleared folds two genuinely different bridge outcomes -- key absent, and key
// present but empty -- into ONE kind, because the bridge cannot label which
// occurred. Both must land on the identical kind: a caller's message may claim
// no more than what the fold actually preserves.
const BakeAnswer clearedFromAbsent = classifyBakeAnswer(std::nullopt, sent);
const BakeAnswer clearedFromEmpty =
classifyBakeAnswer(std::optional<std::string>(""), sent);
CHECK(clearedFromAbsent.kind == BakeAnswerKind::Cleared);
CHECK(clearedFromEmpty.kind == BakeAnswerKind::Cleared);
CHECK(clearedFromAbsent.kind == clearedFromEmpty.kind);
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());
}
// --- answeredOutcome: the guarded read, not switch exhaustiveness alone -------------
{
BakeRequest sent;
sent.instanceGuid = "abcd";
sent.generation = 42;
BakeOutcome landed;
landed.status = BakeStatus::Ok;
landed.sampleId = "bake-1";
landed.generation = sent.generation;
// The one state that may yield a non-null pointer, and it points at the SAME
// outcome classifyBakeAnswer decoded.
const BakeAnswer answered =
classifyBakeAnswer(std::optional<std::string>(encodeBakeOutcome(landed)), sent);
const BakeOutcome* ptr = answeredOutcome(answered);
CHECK(ptr != nullptr);
CHECK(ptr->sampleId == "bake-1");
// Every other real classification nulls out.
CHECK(answeredOutcome(classifyBakeAnswer(std::nullopt, sent)) == nullptr);
CHECK(answeredOutcome(classifyBakeAnswer(
std::optional<std::string>(encodeBakeRequest(sent)), sent)) == nullptr);
CHECK(answeredOutcome(classifyBakeAnswer(std::optional<std::string>("garbage"), sent))
== nullptr);
BakeOutcome foreign = landed;
foreign.generation = sent.generation + 1;
CHECK(answeredOutcome(classifyBakeAnswer(
std::optional<std::string>(encodeBakeOutcome(foreign)), sent)) == nullptr);
// The defensive arm: a hand-built Answered with no outcome set (what a future
// BakeAnswerKind enumerator falling through an un-updated switch would look like
// to this accessor) fails closed rather than being dereferenced.
BakeAnswer malformed;
malformed.kind = BakeAnswerKind::Answered;
CHECK(!malformed.outcome.has_value());
CHECK(answeredOutcome(malformed) == nullptr);
}
// --- kMaxRequestAgeSeconds: a regression pin on the bound itself --------------------
// The bound's meaning depends on the instrument stamping `generation` immediately
// before publishing (AFTER staging the WAV) -- a shell-side ordering fix this pure
// suite cannot observe directly. Pinning the literal at least catches a silent
// widen/narrow of the budget itself.
CHECK(kMaxRequestAgeSeconds == 30);
// --- 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};
BakeScanKey own{true, 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, 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{false}, own, now) ==
BakeScanVerdict::RefuseWrongProject);
// The window before any project is loaded: matchesLoadedProject cannot be true for
// ANY key here — a key can only match a project that is loaded — so this is the
// shell-reachable stand-in for "no book yet", not `own` (which the shell could
// never actually pair with an unloaded session).
CHECK(classifyBakeScan(BakeScanContext{false}, BakeScanKey{true, true, now, false},
now) == BakeScanVerdict::RefuseWrongProject);
// Stale in EITHER direction (a clock that moved backwards counts too).
BakeScanKey old{true, true, now - kMaxRequestAgeSeconds - 1, true};
BakeScanKey future{true, 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, 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, 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{true, false, 0, true};
CHECK(classifyBakeScan(loadedAndActive, notARequest, now) ==
BakeScanVerdict::IgnoreNotARequest);
CHECK(classifyBakeScan(BakeScanContext{false}, notARequest, now) ==
BakeScanVerdict::IgnoreNotARequest);
// A key the enumerator listed but the reader could not return whole is its OWN
// verdict, not folded into the one above: the extension failing to read a request
// that may well be there is a different fault from a key holding something else,
// and neither is visible from the instrument's end.
BakeScanKey unreadable{false, false, 0, true};
CHECK(classifyBakeScan(loadedAndActive, unreadable, now) ==
BakeScanVerdict::IgnoreUnreadable);
CHECK(classifyBakeScan(BakeScanContext{false}, unreadable, now) ==
BakeScanVerdict::IgnoreUnreadable);
// Unreadable outranks even staleness — an age read off a request that was never
// decoded would be a made-up number.
BakeScanKey unreadableOld{false, false, now - kMaxRequestAgeSeconds - 1, true};
CHECK(classifyBakeScan(loadedAndActive, unreadableOld, now) ==
BakeScanVerdict::IgnoreUnreadable);
}
// --- The scan summary: silent ONLY when nothing went unanswered ---------------------
// `answered` is pass-wide and counts a QUEUED write, so gating on it alone let two real
// faults print nothing: a pass that answered another key while skipping ours, and a
// pass whose own answer write was rejected. Those two are what the gate below pins.
{
BakeScanTally clean;
clean.tabsScanned = 1;
clean.keysFound = 1;
clean.activeTabKeys = 1;
clean.answered = 1;
clean.landed = 1;
CHECK(describeBakeScan(clean).empty());
// A refusal is still an answer, so it silences the summary the same way a landing
// does — the refusal's own sentence has already been printed.
BakeScanTally refusedOne = clean;
refusedOne.landed = 0;
CHECK(describeBakeScan(refusedOne).empty());
// THE regression: some OTHER key was answered while ours was skipped. Under the old
// `answered > 0` gate this printed nothing at all, and the instrument then told the
// user a silent console proved the action never ran.
for (int BakeScanTally::*skip :
{&BakeScanTally::unreadable, &BakeScanTally::notARequest,
&BakeScanTally::staleCleared}) {
BakeScanTally mixed = clean;
mixed.keysFound = 2;
mixed.*skip = 1;
const std::string said = describeBakeScan(mixed);
CHECK(!said.empty());
CHECK(said.find("left 1 unanswered") != std::string::npos);
}
// THE other regression: our answer was queued (so `answered` counted it) and the
// SetProjExtState write was rejected. Nothing else went wrong, and it must still
// speak.
BakeScanTally rejected = clean;
rejected.writeFailed = 1;
const std::string wrote = describeBakeScan(rejected);
CHECK(!wrote.empty());
CHECK(wrote.find("1 answer could not be written back") != std::string::npos);
CHECK(wrote.find("sees no answer at all") != std::string::npos);
// Plural agrees, and the count is the tally's own, not a hardcoded 1.
BakeScanTally rejectedTwo = clean;
rejectedTwo.answered = 2;
rejectedTwo.writeFailed = 2;
CHECK(describeBakeScan(rejectedTwo).find("2 answers could not be written back") !=
std::string::npos);
// Nothing found at all: the observation is stated and the candidates listed, with
// no winner picked — the enumeration's visibility of a key set but not yet saved is
// undocumented, so "not the same project's ext state" may not name it.
BakeScanTally nothing;
nothing.tabsScanned = 2;
const std::string none = describeBakeScan(nothing);
CHECK(!none.empty());
CHECK(none.find("2 project tabs") != std::string::npos);
CHECK(none.find("No rsbake_ key was visible") != std::string::npos);
CHECK(none.find("this pass cannot tell which apart") != std::string::npos);
CHECK(none.find("are not reading the same project") == std::string::npos);
CHECK(none.back() == '\n');
// One tab is singular, not "1 project tabs".
BakeScanTally oneTab;
oneTab.tabsScanned = 1;
CHECK(describeBakeScan(oneTab).find("1 project tab.") != std::string::npos);
// Found but unreadable — the fact the old scan discarded.
BakeScanTally unreadable;
unreadable.tabsScanned = 1;
unreadable.keysFound = 1;
unreadable.activeTabKeys = 1;
unreadable.unreadable = 1;
const std::string unread = describeBakeScan(unreadable);
CHECK(unread.find("1 could not be read back") != std::string::npos);
CHECK(unread.find("held something other than a request") == std::string::npos);
// keysFound counts EVERY rsbake_ key, not pending requests only — a label that
// said otherwise produced "3 pending request keys ... 3 held something else".
CHECK(unread.find("1 rsbake_ key") != std::string::npos);
CHECK(unread.find("pending request key") == std::string::npos);
// Found, readable, but not a request.
BakeScanTally foreign;
foreign.tabsScanned = 1;
foreign.keysFound = 1;
foreign.activeTabKeys = 1;
foreign.notARequest = 1;
const std::string other = describeBakeScan(foreign);
CHECK(other.find("1 held something other than a request") != std::string::npos);
CHECK(other.find("could not be read back") == std::string::npos);
// Cleared as stale: an answer was never written, so this too must report.
BakeScanTally stale;
stale.tabsScanned = 1;
stale.keysFound = 1;
stale.activeTabKeys = 1;
stale.staleCleared = 1;
const std::string aged = describeBakeScan(stale);
CHECK(aged.find("past the age bound") != std::string::npos);
// activeTabKeys is REAPER's ACTIVE tab and nothing more. It cannot discriminate the
// multi-tab mis-target — a genuine background-tab request classifies as
// RefuseWrongProject, which is an ANSWER — so no sentence may claim it does, and
// none may call it "the tab this bake was fired against" (whether REAPER makes the
// invoking instance's project current is DAW-unverified).
BakeScanTally elsewhere;
elsewhere.tabsScanned = 2;
elsewhere.keysFound = 1;
elsewhere.activeTabKeys = 0;
elsewhere.notARequest = 1;
const std::string away = describeBakeScan(elsewhere);
CHECK(away.find("0 of them in REAPER's active tab") != std::string::npos);
CHECK(away.find("fired against") == std::string::npos);
}
// --- The per-key line: the only thing that names WHICH key ---------------------------
// The tally counts; it cannot say whose key was skipped. This line is printed for every
// enumerated key, answered or not, and the key carries the asking instance's guid — so
// it is what lets one instance find its own verdict in a multi-instance session.
{
const std::string key = "rsbake_0123abcd";
BakeKeyOutcome landed;
landed.verdict = BakeScanVerdict::Land;
landed.landed = true;
landed.answerWritten = true;
const std::string ok = describeBakeKey(key, landed);
CHECK(ok.find(key) != std::string::npos);
CHECK(ok.find("landed into the bank") != std::string::npos);
CHECK(ok.find("the answer was written back") != std::string::npos);
CHECK(ok.back() == '\n');
// A landing whose answer write was REJECTED — the state the instrument reads as
// Unanswered. This line is the only place it is ever named.
BakeKeyOutcome lost = landed;
lost.answerWritten = false;
const std::string dropped = describeBakeKey(key, lost);
CHECK(dropped.find("landed into the bank") != std::string::npos);
CHECK(dropped.find("could NOT be written back") != std::string::npos);
CHECK(dropped.find("will report no answer") != std::string::npos);
BakeKeyOutcome bankRefused;
bankRefused.verdict = BakeScanVerdict::Land;
bankRefused.landed = false;
bankRefused.answerWritten = true;
CHECK(describeBakeKey(key, bankRefused).find("the landing was refused") !=
std::string::npos);
BakeKeyOutcome wrongTab;
wrongTab.verdict = BakeScanVerdict::RefuseWrongProject;
wrongTab.answerWritten = true;
const std::string refused = describeBakeKey(key, wrongTab);
CHECK(refused.find("not the one this extension has loaded") != std::string::npos);
CHECK(refused.find("the answer was written back") != std::string::npos);
BakeKeyOutcome cleared;
cleared.verdict = BakeScanVerdict::ClearStale;
CHECK(describeBakeKey(key, cleared).find("cleared unanswered") != std::string::npos);
BakeKeyOutcome notRequest;
notRequest.verdict = BakeScanVerdict::IgnoreNotARequest;
CHECK(describeBakeKey(key, notRequest).find("other than a pending request") !=
std::string::npos);
// Absent and Overflow both yield IgnoreUnreadable, but they are different faults to
// go fix, so the line keeps them apart where the verdict cannot.
BakeKeyOutcome empty;
empty.verdict = BakeScanVerdict::IgnoreUnreadable;
BakeKeyOutcome huge = empty;
huge.oversized = true;
const std::string emptyLine = describeBakeKey(key, empty);
const std::string hugeLine = describeBakeKey(key, huge);
CHECK(emptyLine.find("read back empty") != std::string::npos);
CHECK(hugeLine.find("too large to read back whole") != std::string::npos);
CHECK(emptyLine != hugeLine);
// Every line names its own key, so two instances' lines are never confusable.
CHECK(describeBakeKey("rsbake_ffff0000", landed).find("rsbake_ffff0000") !=
std::string::npos);
CHECK(describeBakeKey("rsbake_ffff0000", landed) != ok);
}
// --- 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;
}