Prove every ext-state write by reading the key back, so no guard is constant-true
SetProjExtState's return describes the whole extname. The persist and the instrument's publish now re-read their key; both refusals can finally fire.
This commit is contained in:
+79
-32
@@ -9,11 +9,13 @@
|
||||
// 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; 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.
|
||||
// the persist/upgrade state machine that is the ONLY route to a Banked landing, including
|
||||
// the reachability of its Unpersisted limb at both persist outcomes.
|
||||
|
||||
#include "../src/core/wire/bake_wire.h"
|
||||
|
||||
#include "../src/core/version/app_version.h"
|
||||
#include "../src/core/wire/ext_state_read.h" // extStateWriteLanded (the write-proof peer)
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -31,8 +33,8 @@ static int g_fail = 0;
|
||||
// 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";
|
||||
"the bake reached the bank in memory, but this pass's persist did not confirm its bank "
|
||||
"write, so this answer cannot promise a reload will find it";
|
||||
|
||||
int main() {
|
||||
// --- The exact bytes on the wire -------------------------------------------------
|
||||
@@ -412,13 +414,7 @@ int main() {
|
||||
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
|
||||
@@ -557,11 +553,11 @@ 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);
|
||||
// The sentence names the OBSERVATION behind the flag (see BakeLanding) and stops
|
||||
// there — it may not claim the .rpp on disk already holds the entry.
|
||||
CHECK(ok.find("read back as exactly what this pass wrote") != std::string::npos);
|
||||
CHECK(ok.find(".rpp") == std::string::npos);
|
||||
CHECK(ok.find("carries it") == 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.
|
||||
@@ -589,7 +585,7 @@ 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("persist did not report success") != std::string::npos);
|
||||
CHECK(memoryOnly.find("persist did not confirm its bank write") != 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
|
||||
@@ -633,9 +629,12 @@ int main() {
|
||||
BakeKeyOutcome clearLost = cleared;
|
||||
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("the clear did NOT take") != std::string::npos);
|
||||
CHECK(stuck.find("next pass will see it again") != std::string::npos);
|
||||
CHECK(stuck.find("it was cleared") == std::string::npos);
|
||||
// Rejected also covers a clear that was never issued, so the line may not describe
|
||||
// a read that did not happen.
|
||||
CHECK(stuck.find("read back") == 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;
|
||||
@@ -739,12 +738,11 @@ int main() {
|
||||
}
|
||||
|
||||
// --- 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.
|
||||
// Judging this by SetProjExtState's return made `writeFailed` unproducible on the
|
||||
// landing path and its sentence dead code (extStateWriteLanded owns why). The verdict is
|
||||
// the read-back, which a store that drops the write does produce. The store is modelled
|
||||
// here; the three shells bind these same two calls to SetProjExtState and the
|
||||
// grow-loop read.
|
||||
{
|
||||
struct FakeKeyStore {
|
||||
std::map<std::string, std::string> values;
|
||||
@@ -778,7 +776,7 @@ int main() {
|
||||
rejecting.values[key] = encodeBakeRequest(pending);
|
||||
rejecting.dropWrites = true;
|
||||
rejecting.write(key, answer);
|
||||
CHECK(!bakeWriteLanded(answer, rejecting.read(key)));
|
||||
CHECK(!extStateWriteLanded(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 ==
|
||||
@@ -790,13 +788,12 @@ int main() {
|
||||
tally.keysFound = 1;
|
||||
tally.activeTabKeys = 1;
|
||||
tally.answered = 1;
|
||||
tally.landed = 1;
|
||||
BakeKeyOutcome report;
|
||||
report.verdict = BakeScanVerdict::Land;
|
||||
// The shell reaches Banked only through the upgrade — never by assignment.
|
||||
report.landing = bakeLandingAfterPersist(BakeLanding::Unpersisted, true);
|
||||
report.detail = landedOk.message;
|
||||
report.proof = bakeWriteLanded(answer, rejecting.read(key))
|
||||
report.proof = extStateWriteLanded(answer, rejecting.read(key))
|
||||
? BakeWriteProof::Confirmed
|
||||
: BakeWriteProof::Rejected;
|
||||
if (report.proof == BakeWriteProof::Rejected) ++tally.writeFailed;
|
||||
@@ -811,7 +808,7 @@ int main() {
|
||||
FakeKeyStore accepting;
|
||||
accepting.values[key] = encodeBakeRequest(pending);
|
||||
accepting.write(key, answer);
|
||||
CHECK(bakeWriteLanded(answer, accepting.read(key)));
|
||||
CHECK(extStateWriteLanded(answer, accepting.read(key)));
|
||||
CHECK(classifyBakeAnswer(accepting.read(key), pending).kind ==
|
||||
BakeAnswerKind::Answered);
|
||||
BakeScanTally clean = tally;
|
||||
@@ -823,21 +820,71 @@ int main() {
|
||||
FakeKeyStore clearing;
|
||||
clearing.values[key] = encodeBakeRequest(pending);
|
||||
clearing.write(key, "");
|
||||
CHECK(bakeWriteLanded("", clearing.read(key)));
|
||||
CHECK(extStateWriteLanded("", clearing.read(key)));
|
||||
FakeKeyStore clearDropped;
|
||||
clearDropped.values[key] = encodeBakeRequest(pending);
|
||||
clearDropped.dropWrites = true;
|
||||
clearDropped.write(key, "");
|
||||
CHECK(!bakeWriteLanded("", clearDropped.read(key)));
|
||||
CHECK(!extStateWriteLanded("", 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>(
|
||||
CHECK(!extStateWriteLanded(answer, std::optional<std::string>(
|
||||
answer.substr(0, answer.size() - 1))));
|
||||
CHECK(!bakeWriteLanded(answer, std::optional<std::string>(
|
||||
CHECK(!extStateWriteLanded(answer, std::optional<std::string>(
|
||||
encodeBakeRequest(pending))));
|
||||
CHECK(!bakeWriteLanded(answer, std::nullopt));
|
||||
CHECK(!bakeWriteLanded("", std::optional<std::string>("leftover")));
|
||||
CHECK(!extStateWriteLanded(answer, std::nullopt));
|
||||
CHECK(!extStateWriteLanded("", std::optional<std::string>("leftover")));
|
||||
}
|
||||
|
||||
// --- The Unpersisted limb is REACHABLE, through the same predicate ------------------
|
||||
// `persisted` used to be structurally true wherever a landing could observe it: the
|
||||
// session reported it from SetProjExtState's unread return, and by the time a landing
|
||||
// exists the only two conditions that return could reflect (no active project, an
|
||||
// unsaved one) are already excluded by the Land verdict. So the Unpersisted limb, the
|
||||
// wire answer for it and its console clause were all dead. The session now proves the
|
||||
// `banks` key the same way the landing proves its own — modelled below at both
|
||||
// outcomes, so neither branch is a constant.
|
||||
{
|
||||
const std::string banksJson = R"({"banks":[{"id":"pool","samples":[]}]})";
|
||||
|
||||
// The write REJECTED: the project still holds whatever it held before, which is a
|
||||
// book without the entry this pass just landed in memory.
|
||||
const std::optional<std::string> stale{R"({"banks":[]})"};
|
||||
const bool persistedAfterDrop = extStateWriteLanded(banksJson, stale);
|
||||
CHECK(!persistedAfterDrop);
|
||||
|
||||
BakeKeyOutcome entry;
|
||||
entry.verdict = BakeScanVerdict::Land;
|
||||
entry.landing =
|
||||
bakeLandingAfterPersist(BakeLanding::Unpersisted, persistedAfterDrop);
|
||||
entry.proof = BakeWriteProof::Confirmed; // the ANSWER wrote fine; the persist did not
|
||||
CHECK(entry.landing == BakeLanding::Unpersisted);
|
||||
const std::string line = describeBakeKey("rsbake_0123abcd", entry);
|
||||
CHECK(line.find("IN MEMORY ONLY") != std::string::npos);
|
||||
CHECK(line.find("The answer was written back") != std::string::npos);
|
||||
|
||||
// ...and that landing is answered as a FAILURE on the wire, so no instance adopts
|
||||
// an entry a reload would not find.
|
||||
BakeOutcome answered;
|
||||
answered.status = BakeStatus::Failed;
|
||||
answered.message = kUnpersistedAnswer;
|
||||
answered.generation = 1893456000;
|
||||
const auto back = decodeBakeOutcome(encodeBakeOutcome(answered));
|
||||
CHECK(back.has_value() && back->status == BakeStatus::Failed);
|
||||
CHECK(back.has_value() && back->message == kUnpersistedAnswer);
|
||||
|
||||
// The write ACCEPTED, same predicate, same inputs but for the read-back: the limb
|
||||
// above is a real branch, not a constant.
|
||||
const bool persistedAfterTake =
|
||||
extStateWriteLanded(banksJson, std::optional<std::string>(banksJson));
|
||||
CHECK(persistedAfterTake);
|
||||
CHECK(bakeLandingAfterPersist(BakeLanding::Unpersisted, persistedAfterTake) ==
|
||||
BakeLanding::Banked);
|
||||
// An unreadable read-back must NOT be folded in as an absence-and-therefore-a-clear:
|
||||
// the session routes Overflow to false before it ever reaches this predicate, and a
|
||||
// non-empty write against an absent key is false here regardless.
|
||||
CHECK(!extStateWriteLanded(banksJson, std::nullopt));
|
||||
}
|
||||
|
||||
// --- Every outcome bake_land actually emits survives the key round trip -------------
|
||||
|
||||
Reference in New Issue
Block a user