Make a bake's Banked landing reachable only through the persist upgrade
The dedup path claimed it directly, so a key could be answered Ok pointing at an entry a failed persist never wrote. Write-back is now guarded and three-valued, and landing one request split out of the scan pass.
This commit is contained in:
+139
-15
@@ -7,8 +7,9 @@
|
||||
// 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; 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.
|
||||
// its channel fork; the two key classifiers each end reads the shared key through; the
|
||||
// write-back verdict, driven by a modelled key store that accepts or drops the write; and
|
||||
// the persist/upgrade state machine that is the ONLY route to a Banked landing.
|
||||
|
||||
#include "../src/core/wire/bake_wire.h"
|
||||
|
||||
@@ -26,6 +27,13 @@ static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// The one answer bake_land emits for a landing its pass could not persist. Pinned once
|
||||
// here and used by both blocks below that need it, so the test suite does not become a
|
||||
// third place the sentence lives.
|
||||
static const std::string kUnpersistedAnswer =
|
||||
"the bake reached the bank in memory, but this pass's persist did not report success, "
|
||||
"so this answer cannot promise a reload will find it";
|
||||
|
||||
int main() {
|
||||
// --- The exact bytes on the wire -------------------------------------------------
|
||||
// A round-trip alone would pass a reordered or inserted field; the tags exist to guard
|
||||
@@ -442,6 +450,24 @@ int main() {
|
||||
CHECK(describeBakeScan(rejectedTwo).find("2 answers could not be written back") !=
|
||||
std::string::npos);
|
||||
|
||||
// An answer whose write-and-check THREW is neither landed nor known-failed, and it
|
||||
// must break the silence too: the whole point of the gate is that no fault in the
|
||||
// pass leaves the asking instance with a no-answer and an empty console.
|
||||
BakeScanTally unproven = clean;
|
||||
unproven.writeUnproven = 1;
|
||||
const std::string unsure = describeBakeScan(unproven);
|
||||
CHECK(!unsure.empty());
|
||||
CHECK(unsure.find("1 answer could not be checked after writing") !=
|
||||
std::string::npos);
|
||||
CHECK(unsure.find("is unknown") != std::string::npos);
|
||||
// ...and it is NOT reported as a rejection, which is a different claim.
|
||||
CHECK(unsure.find("could not be written back") == std::string::npos);
|
||||
BakeScanTally unprovenTwo = clean;
|
||||
unprovenTwo.answered = 2;
|
||||
unprovenTwo.writeUnproven = 2;
|
||||
CHECK(describeBakeScan(unprovenTwo).find("2 answers could not be checked") !=
|
||||
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.
|
||||
@@ -521,7 +547,7 @@ int main() {
|
||||
BakeKeyOutcome landed;
|
||||
landed.verdict = BakeScanVerdict::Land;
|
||||
landed.landing = BakeLanding::Banked;
|
||||
landed.writeConfirmed = true;
|
||||
landed.proof = BakeWriteProof::Confirmed;
|
||||
landed.detail = "added as a distinct capture";
|
||||
const std::string ok = describeBakeKey(key, landed);
|
||||
CHECK(ok.find(key) != std::string::npos);
|
||||
@@ -531,16 +557,31 @@ int main() {
|
||||
// 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);
|
||||
// Banked claims the persist FLAG and nothing more, never that the .rpp already
|
||||
// holds it — saveToActiveProject issues its SetProjExtState calls without reading
|
||||
// their returns, and project ext state reaches the file on the project's own save.
|
||||
CHECK(ok.find("persist reported success") != std::string::npos);
|
||||
CHECK(ok.find("once the project is saved") != 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.writeConfirmed = false;
|
||||
lost.proof = BakeWriteProof::Rejected;
|
||||
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 write-and-check itself failing is a THIRD state, not a rejection: claiming
|
||||
// the answer did not land would be a fact this pass never observed.
|
||||
BakeKeyOutcome unchecked = landed;
|
||||
unchecked.proof = BakeWriteProof::Unknown;
|
||||
const std::string unsure = describeBakeKey(key, unchecked);
|
||||
CHECK(unsure.find("is unknown") != std::string::npos);
|
||||
CHECK(unsure.find("could NOT be written back") == std::string::npos);
|
||||
CHECK(unsure.find("The answer was written back") == std::string::npos);
|
||||
CHECK(unsure != ok && unsure != dropped);
|
||||
|
||||
// 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.
|
||||
@@ -548,8 +589,12 @@ int main() {
|
||||
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.find("persist did not report success") != std::string::npos);
|
||||
CHECK(memoryOnly != ok);
|
||||
// It may NOT claim what the project's saved state holds: the persist can fail
|
||||
// before writing anything or throw part-way, and a dedup hit's target may have
|
||||
// been in the project since long before this pass.
|
||||
CHECK(memoryOnly.find("does not carry it") == std::string::npos);
|
||||
|
||||
BakeKeyOutcome partial = landed;
|
||||
partial.landing = BakeLanding::Partial;
|
||||
@@ -562,7 +607,7 @@ int main() {
|
||||
BakeKeyOutcome bankRefused;
|
||||
bankRefused.verdict = BakeScanVerdict::Land;
|
||||
bankRefused.landing = BakeLanding::Refused;
|
||||
bankRefused.writeConfirmed = true;
|
||||
bankRefused.proof = BakeWriteProof::Confirmed;
|
||||
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);
|
||||
@@ -571,7 +616,7 @@ int main() {
|
||||
|
||||
BakeKeyOutcome wrongTab;
|
||||
wrongTab.verdict = BakeScanVerdict::RefuseWrongProject;
|
||||
wrongTab.writeConfirmed = true;
|
||||
wrongTab.proof = BakeWriteProof::Confirmed;
|
||||
wrongTab.detail =
|
||||
"this bake's project tab is not the one the extension has loaded -- focus that "
|
||||
"tab and try again";
|
||||
@@ -583,14 +628,21 @@ int main() {
|
||||
// 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;
|
||||
cleared.proof = BakeWriteProof::Confirmed;
|
||||
CHECK(describeBakeKey(key, cleared).find("cleared unanswered") != std::string::npos);
|
||||
BakeKeyOutcome clearLost = cleared;
|
||||
clearLost.writeConfirmed = false;
|
||||
clearLost.proof = BakeWriteProof::Rejected;
|
||||
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);
|
||||
// And the clear's third state, same as an answer's: unchecked is not disproven.
|
||||
BakeKeyOutcome clearUnchecked = cleared;
|
||||
clearUnchecked.proof = BakeWriteProof::Unknown;
|
||||
const std::string maybeCleared = describeBakeKey(key, clearUnchecked);
|
||||
CHECK(maybeCleared.find("whether the clear took is unknown") != std::string::npos);
|
||||
CHECK(maybeCleared.find("it was cleared unanswered") == std::string::npos);
|
||||
CHECK(maybeCleared.find("could NOT be read back") == std::string::npos);
|
||||
|
||||
BakeKeyOutcome notRequest;
|
||||
notRequest.verdict = BakeScanVerdict::IgnoreNotARequest;
|
||||
@@ -613,6 +665,77 @@ int main() {
|
||||
CHECK(describeBakeKey("rsbake_ffff0000", landed).find("rsbake_ffff0000") !=
|
||||
std::string::npos);
|
||||
CHECK(describeBakeKey("rsbake_ffff0000", landed) != ok);
|
||||
|
||||
// A LANDING this build has no word for must not be reported as a VERDICT gap: the
|
||||
// two are different enums and send a reader to two different places.
|
||||
BakeKeyOutcome futureLanding = landed;
|
||||
futureLanding.landing = static_cast<BakeLanding>(99);
|
||||
const std::string unnamed = describeBakeKey(key, futureLanding);
|
||||
CHECK(unnamed.find("a landing state this build has no word for") !=
|
||||
std::string::npos);
|
||||
CHECK(unnamed.find("a verdict this build has no word for") == std::string::npos);
|
||||
// A VERDICT gap still reports as one.
|
||||
BakeKeyOutcome futureVerdict;
|
||||
futureVerdict.verdict = static_cast<BakeScanVerdict>(99);
|
||||
CHECK(describeBakeKey(key, futureVerdict).find(
|
||||
"a verdict this build has no word for") != std::string::npos);
|
||||
}
|
||||
|
||||
// --- The persist/upgrade state machine: the ONE route to Banked -----------------------
|
||||
// The shell assigns every Land verdict's landing without knowing whether the pass's
|
||||
// persist ran, then threads the flag through here. The bug this pins: a dedup hit used
|
||||
// to be answered Banked directly, on the grounds that it changed nothing — false
|
||||
// exactly when the entry it deduped against was one the SAME pass had just added and
|
||||
// then failed to persist, which answers Ok for an entry the project does not carry.
|
||||
{
|
||||
// A dedup hit and a fresh add are INDISTINGUISHABLE here, by construction: the shell
|
||||
// assigns Unpersisted to both, so both need the same observation to be promoted.
|
||||
CHECK(bakeLandingAfterPersist(BakeLanding::Unpersisted, true) == BakeLanding::Banked);
|
||||
CHECK(bakeLandingAfterPersist(BakeLanding::Unpersisted, false) ==
|
||||
BakeLanding::Unpersisted);
|
||||
|
||||
// Exhaustive over both arguments: (Unpersisted, persisted) is the ONLY pair that
|
||||
// produces Banked from anything else, and nothing else is altered at all — so a
|
||||
// persist that ran cannot launder a refusal or a half-written landing into a
|
||||
// success, and a persist that did not cannot demote one.
|
||||
for (const BakeLanding from : {BakeLanding::Refused, BakeLanding::Partial,
|
||||
BakeLanding::Unpersisted, BakeLanding::Banked}) {
|
||||
for (const bool persisted : {false, true}) {
|
||||
const BakeLanding to = bakeLandingAfterPersist(from, persisted);
|
||||
const bool promotes = from == BakeLanding::Unpersisted && persisted;
|
||||
CHECK(to == (promotes ? BakeLanding::Banked : from));
|
||||
CHECK((to == BakeLanding::Banked) ==
|
||||
(promotes || from == BakeLanding::Banked));
|
||||
}
|
||||
}
|
||||
|
||||
// A dedup hit driven end to end at both persist outcomes: what the instrument is
|
||||
// told and what the console prints both follow the flag, and they never disagree.
|
||||
for (const bool persisted : {true, false}) {
|
||||
BakeKeyOutcome deduped;
|
||||
deduped.verdict = BakeScanVerdict::Land;
|
||||
deduped.landing = bakeLandingAfterPersist(BakeLanding::Unpersisted, persisted);
|
||||
|
||||
// A landing the pass could not persist is re-encoded as a FAILURE, not the Ok
|
||||
// it was headed for, so no instance adopts an entry a reload may not find.
|
||||
BakeOutcome answer;
|
||||
answer.status = BakeStatus::Ok;
|
||||
answer.sampleId = "bake-1";
|
||||
answer.generation = 1893456000;
|
||||
if (deduped.landing == BakeLanding::Unpersisted) {
|
||||
answer.status = BakeStatus::Failed;
|
||||
answer.message = kUnpersistedAnswer;
|
||||
}
|
||||
const auto back = decodeBakeOutcome(encodeBakeOutcome(answer));
|
||||
CHECK(back.has_value());
|
||||
CHECK(back.has_value() &&
|
||||
back->status == (persisted ? BakeStatus::Ok : BakeStatus::Failed));
|
||||
|
||||
// The console line and the wire answer never disagree about the same key.
|
||||
deduped.proof = BakeWriteProof::Confirmed;
|
||||
const std::string line = describeBakeKey("rsbake_0123abcd", deduped);
|
||||
CHECK((line.find("IN MEMORY ONLY") != std::string::npos) == !persisted);
|
||||
}
|
||||
}
|
||||
|
||||
// --- A rejected write is REACHABLE, and it is what the tally and the line come from ---
|
||||
@@ -670,10 +793,13 @@ int main() {
|
||||
tally.landed = 1;
|
||||
BakeKeyOutcome report;
|
||||
report.verdict = BakeScanVerdict::Land;
|
||||
report.landing = BakeLanding::Banked;
|
||||
// The shell reaches Banked only through the upgrade — never by assignment.
|
||||
report.landing = bakeLandingAfterPersist(BakeLanding::Unpersisted, true);
|
||||
report.detail = landedOk.message;
|
||||
report.writeConfirmed = bakeWriteLanded(answer, rejecting.read(key));
|
||||
if (!report.writeConfirmed) ++tally.writeFailed;
|
||||
report.proof = bakeWriteLanded(answer, rejecting.read(key))
|
||||
? BakeWriteProof::Confirmed
|
||||
: BakeWriteProof::Rejected;
|
||||
if (report.proof == BakeWriteProof::Rejected) ++tally.writeFailed;
|
||||
CHECK(tally.writeFailed == 1);
|
||||
CHECK(describeBakeKey(key, report).find("could NOT be written back") !=
|
||||
std::string::npos);
|
||||
@@ -769,9 +895,7 @@ int main() {
|
||||
// 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";
|
||||
unpersisted.message = kUnpersistedAnswer;
|
||||
|
||||
BakeOutcome partial = noProject;
|
||||
partial.status = BakeStatus::Failed;
|
||||
|
||||
Reference in New Issue
Block a user