Prove the bake's answer writes by reading the key back
SetProjExtState's return covers the whole extname, so it never saw one key. The persist verdict now reaches the report, and a throw mid-write no longer claims the landing left nothing behind.
This commit is contained in:
+168
-14
@@ -7,7 +7,8 @@
|
||||
// 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.
|
||||
// its channel fork; the two key classifiers each end reads the shared key through; and the
|
||||
// write-back verdict, driven by a modelled key store that accepts or drops the write.
|
||||
|
||||
#include "../src/core/wire/bake_wire.h"
|
||||
|
||||
@@ -15,6 +16,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
@@ -482,7 +484,9 @@ int main() {
|
||||
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.
|
||||
// Cleared as stale: an answer was never written, so this too must report. The
|
||||
// summary may NOT say the clears were written — whether each one took is a per-key
|
||||
// read-back, and only describeBakeKey has seen it.
|
||||
BakeScanTally stale;
|
||||
stale.tabsScanned = 1;
|
||||
stale.keysFound = 1;
|
||||
@@ -490,6 +494,7 @@ int main() {
|
||||
stale.staleCleared = 1;
|
||||
const std::string aged = describeBakeScan(stale);
|
||||
CHECK(aged.find("past the age bound") != std::string::npos);
|
||||
CHECK(aged.find("were cleared") == 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
|
||||
@@ -515,40 +520,77 @@ int main() {
|
||||
|
||||
BakeKeyOutcome landed;
|
||||
landed.verdict = BakeScanVerdict::Land;
|
||||
landed.landed = true;
|
||||
landed.answerWritten = true;
|
||||
landed.landing = BakeLanding::Banked;
|
||||
landed.writeConfirmed = true;
|
||||
landed.detail = "added as a distinct capture";
|
||||
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.find("The answer was written back") != std::string::npos);
|
||||
CHECK(ok.back() == '\n');
|
||||
// The outcome's own reason rides the SAME line: one self-contained line per key,
|
||||
// rather than a keyed verdict and an unkeyed reason the reader has to pair up.
|
||||
CHECK(ok.find("(added as a distinct capture)") != std::string::npos);
|
||||
|
||||
// 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;
|
||||
lost.writeConfirmed = 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);
|
||||
|
||||
// The four landing states are four different lines: a bake the project never
|
||||
// persisted, and one that threw mid-write, may not read as a clean success or as a
|
||||
// clean refusal.
|
||||
BakeKeyOutcome unpersisted = landed;
|
||||
unpersisted.landing = BakeLanding::Unpersisted;
|
||||
const std::string memoryOnly = describeBakeKey(key, unpersisted);
|
||||
CHECK(memoryOnly.find("IN MEMORY ONLY") != std::string::npos);
|
||||
CHECK(memoryOnly.find("could not persist it") != std::string::npos);
|
||||
CHECK(memoryOnly != ok);
|
||||
|
||||
BakeKeyOutcome partial = landed;
|
||||
partial.landing = BakeLanding::Partial;
|
||||
partial.detail = "the landing failed while writing: bad allocation";
|
||||
const std::string half = describeBakeKey(key, partial);
|
||||
CHECK(half.find("after it had begun writing") != std::string::npos);
|
||||
CHECK(half.find("may have left a file") != std::string::npos);
|
||||
CHECK(half.find("the landing was refused") == 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);
|
||||
bankRefused.landing = BakeLanding::Refused;
|
||||
bankRefused.writeConfirmed = true;
|
||||
bankRefused.detail = "the bank refused the new capture";
|
||||
const std::string refusedLine = describeBakeKey(key, bankRefused);
|
||||
CHECK(refusedLine.find("the landing was refused") != std::string::npos);
|
||||
// The refusal's REASON is what the keyed line used to lack entirely.
|
||||
CHECK(refusedLine.find("(the bank refused the new capture)") != std::string::npos);
|
||||
|
||||
BakeKeyOutcome wrongTab;
|
||||
wrongTab.verdict = BakeScanVerdict::RefuseWrongProject;
|
||||
wrongTab.answerWritten = true;
|
||||
wrongTab.writeConfirmed = true;
|
||||
wrongTab.detail =
|
||||
"this bake's project tab is not the one the extension has loaded -- focus that "
|
||||
"tab and try again";
|
||||
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);
|
||||
CHECK(refused.find("not the one the extension has loaded") != std::string::npos);
|
||||
CHECK(refused.find("focus that tab and try again") != std::string::npos);
|
||||
CHECK(refused.find("The answer was written back") != std::string::npos);
|
||||
|
||||
// A clear is a write too: it may be claimed only where it was read back.
|
||||
BakeKeyOutcome cleared;
|
||||
cleared.verdict = BakeScanVerdict::ClearStale;
|
||||
cleared.writeConfirmed = true;
|
||||
CHECK(describeBakeKey(key, cleared).find("cleared unanswered") != std::string::npos);
|
||||
BakeKeyOutcome clearLost = cleared;
|
||||
clearLost.writeConfirmed = false;
|
||||
const std::string stuck = describeBakeKey(key, clearLost);
|
||||
CHECK(stuck.find("the clear could NOT be read back") != std::string::npos);
|
||||
CHECK(stuck.find("next pass will see it again") != std::string::npos);
|
||||
CHECK(stuck.find("it was cleared") == std::string::npos);
|
||||
|
||||
BakeKeyOutcome notRequest;
|
||||
notRequest.verdict = BakeScanVerdict::IgnoreNotARequest;
|
||||
@@ -573,6 +615,105 @@ int main() {
|
||||
CHECK(describeBakeKey("rsbake_ffff0000", landed) != ok);
|
||||
}
|
||||
|
||||
// --- A rejected write is REACHABLE, and it is what the tally and the line come from ---
|
||||
// The shell used to judge this by SetProjExtState's return, which is the size of the
|
||||
// WHOLE extname's state — `banks` alone keeps that non-zero in every case a bake can
|
||||
// reach, so `writeFailed` could not be produced at all and the sentence for it was dead
|
||||
// code. The verdict is now the read-back below, which a store that drops the write does
|
||||
// produce. The store is modelled here; the shell binds these same two calls to
|
||||
// SetProjExtState and its existing grow-loop GetProjExtState read.
|
||||
{
|
||||
struct FakeKeyStore {
|
||||
std::map<std::string, std::string> values;
|
||||
bool dropWrites = false;
|
||||
|
||||
void write(const std::string& k, const std::string& v) {
|
||||
if (dropWrites) return; // REAPER rejected it
|
||||
if (v.empty()) values.erase(k); // SetProjExtState("") deletes
|
||||
else values[k] = v;
|
||||
}
|
||||
std::optional<std::string> read(const std::string& k) const {
|
||||
const auto it = values.find(k);
|
||||
return it == values.end() ? std::nullopt
|
||||
: std::optional<std::string>(it->second);
|
||||
}
|
||||
};
|
||||
|
||||
const std::string key = "rsbake_0123abcd";
|
||||
BakeRequest pending;
|
||||
pending.instanceGuid = "0123abcd";
|
||||
pending.generation = 1893456000;
|
||||
BakeOutcome landedOk;
|
||||
landedOk.status = BakeStatus::Ok;
|
||||
landedOk.sampleId = "bake-1";
|
||||
landedOk.message = "added as a distinct capture";
|
||||
landedOk.generation = pending.generation;
|
||||
const std::string answer = encodeBakeOutcome(landedOk);
|
||||
|
||||
// Rejected: the key still holds the request the instrument left there.
|
||||
FakeKeyStore rejecting;
|
||||
rejecting.values[key] = encodeBakeRequest(pending);
|
||||
rejecting.dropWrites = true;
|
||||
rejecting.write(key, answer);
|
||||
CHECK(!bakeWriteLanded(answer, rejecting.read(key)));
|
||||
// ...which is exactly what the ASKING end reads as a no-answer. The two classifiers
|
||||
// agree about this one state, which is why the extension has to name it.
|
||||
CHECK(classifyBakeAnswer(rejecting.read(key), pending).kind ==
|
||||
BakeAnswerKind::Unanswered);
|
||||
|
||||
// The shell's write loop from that verdict through to both strings it prints.
|
||||
BakeScanTally tally;
|
||||
tally.tabsScanned = 1;
|
||||
tally.keysFound = 1;
|
||||
tally.activeTabKeys = 1;
|
||||
tally.answered = 1;
|
||||
tally.landed = 1;
|
||||
BakeKeyOutcome report;
|
||||
report.verdict = BakeScanVerdict::Land;
|
||||
report.landing = BakeLanding::Banked;
|
||||
report.detail = landedOk.message;
|
||||
report.writeConfirmed = bakeWriteLanded(answer, rejecting.read(key));
|
||||
if (!report.writeConfirmed) ++tally.writeFailed;
|
||||
CHECK(tally.writeFailed == 1);
|
||||
CHECK(describeBakeKey(key, report).find("could NOT be written back") !=
|
||||
std::string::npos);
|
||||
CHECK(describeBakeScan(tally).find("1 answer could not be written back") !=
|
||||
std::string::npos);
|
||||
|
||||
// The same store ACCEPTING the write: the verdict flips, the tally stays at zero and
|
||||
// the summary goes silent — so neither answer above is a constant.
|
||||
FakeKeyStore accepting;
|
||||
accepting.values[key] = encodeBakeRequest(pending);
|
||||
accepting.write(key, answer);
|
||||
CHECK(bakeWriteLanded(answer, accepting.read(key)));
|
||||
CHECK(classifyBakeAnswer(accepting.read(key), pending).kind ==
|
||||
BakeAnswerKind::Answered);
|
||||
BakeScanTally clean = tally;
|
||||
clean.writeFailed = 0;
|
||||
CHECK(describeBakeScan(clean).empty());
|
||||
|
||||
// A CLEAR is proven the other way round: it lands as an ABSENT key, and a dropped
|
||||
// clear leaves the stale request standing for the next pass to find.
|
||||
FakeKeyStore clearing;
|
||||
clearing.values[key] = encodeBakeRequest(pending);
|
||||
clearing.write(key, "");
|
||||
CHECK(bakeWriteLanded("", clearing.read(key)));
|
||||
FakeKeyStore clearDropped;
|
||||
clearDropped.values[key] = encodeBakeRequest(pending);
|
||||
clearDropped.dropWrites = true;
|
||||
clearDropped.write(key, "");
|
||||
CHECK(!bakeWriteLanded("", clearDropped.read(key)));
|
||||
|
||||
// Byte equality is the claim the line makes: a truncated or foreign value under the
|
||||
// key is not the answer we wrote, and an unreadable key proves nothing at all.
|
||||
CHECK(!bakeWriteLanded(answer, std::optional<std::string>(
|
||||
answer.substr(0, answer.size() - 1))));
|
||||
CHECK(!bakeWriteLanded(answer, std::optional<std::string>(
|
||||
encodeBakeRequest(pending))));
|
||||
CHECK(!bakeWriteLanded(answer, std::nullopt));
|
||||
CHECK(!bakeWriteLanded("", std::optional<std::string>("leftover")));
|
||||
}
|
||||
|
||||
// --- 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.
|
||||
@@ -623,9 +764,22 @@ int main() {
|
||||
writeFailed.status = BakeStatus::Failed;
|
||||
writeFailed.message = "could not write the bake into the bank folder";
|
||||
|
||||
// A landing the project would not persist is answered as a FAILURE, not as the Ok
|
||||
// it was on its way to being: the entry exists in memory only, so an instance that
|
||||
// adopted it would be pointing at something no reload will have.
|
||||
BakeOutcome unpersisted = noProject;
|
||||
unpersisted.status = BakeStatus::Failed;
|
||||
unpersisted.message =
|
||||
"the bake reached the bank in memory, but this pass could not persist it, so "
|
||||
"the project's saved bank state does not carry it";
|
||||
|
||||
BakeOutcome partial = noProject;
|
||||
partial.status = BakeStatus::Failed;
|
||||
partial.message = "the landing failed while writing: bad allocation";
|
||||
|
||||
for (const BakeOutcome& emitted :
|
||||
{added, replaced, deduped, noProject, stagedMissing, noSource, indexRejected,
|
||||
wrongProject, writeFailed}) {
|
||||
wrongProject, writeFailed, unpersisted, partial}) {
|
||||
const auto back = decodeBakeOutcome(encodeBakeOutcome(emitted));
|
||||
CHECK(back.has_value());
|
||||
CHECK(back.has_value() && *back == emitted);
|
||||
|
||||
Reference in New Issue
Block a user