[Test] (Pipe): pin the resource record's lifecycle and the buffer sub-data range encoding at both of its bounds

This commit is contained in:
2026-09-08 03:51:21 -04:00
parent 6eb0e675ad
commit 355c60b901
+543 -5
View File
@@ -12,11 +12,12 @@
// Two packages fill this file in and neither of them touches MG_Test/Pipe/CMakeLists.txt to
// do it: the applier-side cases (a create marks the slot live, a respecify replaces the
// descriptor and bumps Serial, a destroy clears Live, a stale generation resolves to nothing,
// HasLiveHostWrites is false on every path this phase has, and the sub-data range encoding at
// both of its bounds with the emitter's splitter over it) belong to the branch that gives the
// entry points their bodies; the emitter-side cases (the sticky BindMask over every buffer
// target, on create AND on a following respecify; the slot released at destruction) belong to
// the client branch. They are disjoint TEST bodies in one file.
// HasLiveHostWrites is false on every path this phase has, and the sub-data range ENCODING at
// both of its bounds) belong to the branch that gives the entry points their bodies; the
// emitter-side cases (the sticky BindMask over every buffer target, on create AND on a
// following respecify; the slot released at destruction; and the SPLITTER over that encoding,
// which lives in the client's ResourceTracker) belong to the client branch. They are disjoint
// TEST bodies in one file.
//
// THE SUITE IS `ResourceEmit`, not `ResourceEmitTest`: the file is XTest.cpp and the suite is
// X, which is this directory's convention (RenderStateSpansTest.cpp -> RenderStateSpans), and
@@ -34,14 +35,21 @@
#include <gtest/gtest.h>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <system_error>
#if defined(_WIN32)
#include <process.h>
#define MGTEST_HAVE_FORK 0
#else
#include <csignal>
#include <sys/wait.h>
#include <unistd.h>
#define MGTEST_HAVE_FORK 1
#endif
#include "Includes.h"
@@ -64,6 +72,13 @@ namespace {
#endif
}
std::string ReadLog() {
std::ifstream in(g_logPath, std::ios::binary);
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
// The op table is INSTALLED BY A BACKEND, at its own bring-up, and uninstalled at its
// teardown - it is not part of the applier's state and MGPipeApplierReset deliberately
// does not clear it. A process with no backend in it therefore has none, and that is the
@@ -91,6 +106,529 @@ namespace {
MGPipeSetResourceOps(nullptr);
EXPECT_EQ(MGPipeGetResourceOps(), nullptr);
#endif
}
// =====================================================================================
// The applier's record lifecycle.
//
// WHAT THESE CASES CAN SEE, AND WHY THEY ARE ENOUGH. The applier's whole job in this
// family is identity, extent and order: which slot is live, what its declared storage is,
// which calls move its serial, and which calls are refused before a backend is handed a
// range it would read or write outside that storage. All four are answerable from
// MGPipeApplier() with no context, no device and no emitter - the emitter's half (a real
// BufferObject minting a handle, the sticky bind mask, the range splitter) is the client
// package's, and its cases are appended to this file beside these.
//
// THE SPLITTER IS NOT TESTED HERE, deliberately: it lives in the client's ResourceTracker
// and does not exist yet on this branch. What IS tested here is the thing the splitter is
// written against - the range ENCODING and its two bounds - so that the case which proves
// the split is contiguous, non-overlapping and reassembles has a pinned bound to split at.
// =====================================================================================
#if MOBILEGL_PIPE_PUSH
// A fresh applier per case, and no table left installed behind one. Every case is its own
// process under ctest, so this is belt and braces - but running the binary by hand must
// give the same answers as running it under ctest, or a failure cannot be reproduced.
struct ApplierGuard {
ApplierGuard() {
MGPipeSetResourceOps(nullptr);
MGPipeApplierReset();
}
~ApplierGuard() {
MGPipeSetResourceOps(nullptr);
MGPipeApplierReset();
}
};
MGPResourceDesc BufferDesc(MGPipeHandle res, Uint32 width, Uint32 glName) {
MGPResourceDesc desc{};
desc.Resource = res;
desc.Width = width;
desc.GlNameForDiag = glName;
return desc;
}
MGPHandleOnly BufferHandle(MGPipeHandle res) {
return MGPHandleOnly{res, static_cast<Uint32>(MGPipeKind::Buffer), 0};
}
// Built the way the emitter will build it: the destination range goes in through
// MGPipeSetSubDataBufferRange and nothing else touches the box.
MGPSubData BufferWrite(MGPipeHandle res, Uint64 offset, Uint64 size) {
MGPSubData record{};
record.Res = res;
EXPECT_TRUE(MGPipeSetSubDataBufferRange(record, offset, size));
return record;
}
// Re-read rather than held: a create can grow the record vector and invalidate a
// reference taken before it.
const MGPipeResourceRecord& RecordOf(Uint32 slot) {
EXPECT_GT(MGPipeApplier().Resources.size(), static_cast<SizeT>(slot));
return MGPipeApplier().Resources[slot];
}
// The backend's half, as a table that only counts. It is what proves the dispatch is BY
// HANDLE - no frontend object reaches it, and the handle it is given is the one the record
// names.
struct SpyState {
Uint32 Creates = 0;
Uint32 Respecifies = 0;
Uint32 SubDatas = 0;
Uint32 Residents = 0;
Uint32 Flushes = 0;
Uint32 Readbacks = 0;
Uint32 Destroys = 0;
Uint32 Maps = 0;
Uint32 Unmaps = 0;
MGPipeHandle LastHandle = kMGPipeNullHandle;
Uint64 LastMapSize = 0;
};
SpyState g_spy;
Uint8 g_spyMapTarget = 0;
void SpyCreate(MGPipeHandle res, const MGPResourceDesc&) {
++g_spy.Creates;
g_spy.LastHandle = res;
}
void SpyRespecify(MGPipeHandle res, const MGPResourceDesc&, const void*) {
++g_spy.Respecifies;
g_spy.LastHandle = res;
}
void SpySubData(MGPipeHandle res, const MGPSubData&, const void*) {
++g_spy.SubDatas;
g_spy.LastHandle = res;
}
void SpyResident(MGPipeHandle res, const MGPSubData&, const void*) {
++g_spy.Residents;
g_spy.LastHandle = res;
}
void SpyFlush(MGPipeHandle res, const MGPFlushRange&, const void*) {
++g_spy.Flushes;
g_spy.LastHandle = res;
}
void SpyReadback(MGPipeHandle res, const MGPReadback&) {
++g_spy.Readbacks;
g_spy.LastHandle = res;
}
void SpyDestroy(MGPipeHandle res) {
++g_spy.Destroys;
g_spy.LastHandle = res;
}
void* SpyMap(MGPipeHandle res, Uint64 size, const void*) {
++g_spy.Maps;
g_spy.LastHandle = res;
g_spy.LastMapSize = size;
return &g_spyMapTarget;
}
void SpyUnmap(MGPipeHandle res) {
++g_spy.Unmaps;
g_spy.LastHandle = res;
}
const MGPipeResourceOps kSpyOps{SpyCreate, SpyRespecify, SpySubData, SpyResident, SpyFlush,
SpyReadback, SpyDestroy, SpyMap, SpyUnmap};
#if MGTEST_HAVE_FORK
struct ChildResult {
int Status = -1;
std::string Log;
};
// PipeInputsTest's and RenderStateSpansTest's shape, and their reason: gtest's own death
// tests are not used in this repository. The log file is removed first and the whole of
// what the child left in it is what comes back, so a second child in one process cannot
// read the first one's line.
template <class Body>
ChildResult RunInChild(Body body) {
ChildResult result;
std::error_code ec;
std::filesystem::remove(g_logPath, ec);
std::fflush(nullptr);
const pid_t pid = ::fork();
if (pid < 0) return result;
if (pid == 0) {
body();
::_exit(0);
}
int status = 0;
if (::waitpid(pid, &status, 0) != pid) return result;
result.Status = status;
result.Log = ReadLog();
return result;
}
Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; }
std::string DescribeStatus(const ChildResult& r) {
if (r.Status < 0) return "fork/waitpid failed";
if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status));
if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status));
return "status " + std::to_string(r.Status);
}
#endif // MGTEST_HAVE_FORK
#endif // MOBILEGL_PIPE_PUSH
// A create is emitted from the buffer object's CONSTRUCTOR, so it defines no storage and
// is not a mutation: it says a resource of this identity exists. Slot 0 is the reserved
// null handle and never becomes live, whatever a record says.
TEST(ResourceEmit, ACreateMarksTheSlotLiveAndCarriesItsDescriptor) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
#else
ApplierGuard guard;
const MGPipeHandle res{7, 3};
MGPipeApplyResourceCreate(BufferDesc(res, 0, 41));
const MGPipeResourceRecord& record = RecordOf(res.Slot);
EXPECT_TRUE(record.Live);
EXPECT_EQ(record.Gen, res.Gen);
EXPECT_EQ(record.Desc.GlNameForDiag, 41u);
EXPECT_EQ(record.Desc.Width, 0u) << "a create defines no storage; the first respecify does";
EXPECT_EQ(record.Serial, 0u) << "a create is not a mutation, and a fresh backend twin "
"starts its own synced serial at 0";
EXPECT_FALSE(record.HasLiveHostWrites);
// The slots below the named one are reachable and are NOT live: growing the table is
// not the same as populating it.
EXPECT_FALSE(MGPipeApplier().Resources[0].Live);
EXPECT_FALSE(MGPipeApplier().Resources[res.Slot - 1].Live);
// And the reserved handle is refused rather than made live.
MGPipeApplyResourceCreate(BufferDesc(kMGPipeNullHandle, 4096, 0));
EXPECT_FALSE(MGPipeApplier().Resources[0].Live);
#endif
}
// The serial is the server-owned MGGen the backend twin compares against instead of
// mirroring a frontend change serial. Exactly the four mutations move it; a readback and a
// persistent-map acquisition do not, because neither changes what is in the store.
TEST(ResourceEmit, ARespecifyReplacesTheDescriptorAndOnlyAMutationMovesTheSerial) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
#else
ApplierGuard guard;
const MGPipeHandle res{7, 3};
const Uint8 bytes[64] = {};
MGPipeApplyResourceCreate(BufferDesc(res, 0, 41));
MGPResourceDesc mutableStore = BufferDesc(res, 1024, 41);
mutableStore.HasDefinedContent = 1;
MGPipeApplyResourceRespecify(mutableStore, bytes);
EXPECT_EQ(RecordOf(res.Slot).Desc.Width, 1024u);
EXPECT_EQ(RecordOf(res.Slot).Serial, 1u);
// A respecify REPLACES the descriptor - it does not merge into it - so an immutable
// store that shrinks is described as an immutable store that shrank.
MGPResourceDesc immutableStore = BufferDesc(res, 512, 41);
immutableStore.Immutable = 1;
MGPipeApplyResourceRespecify(immutableStore, nullptr);
EXPECT_EQ(RecordOf(res.Slot).Desc.Width, 512u);
EXPECT_EQ(RecordOf(res.Slot).Desc.Immutable, 1u);
EXPECT_EQ(RecordOf(res.Slot).Desc.HasDefinedContent, 0u);
EXPECT_EQ(RecordOf(res.Slot).Serial, 2u);
// The per-record half of the call's kNeedsAck follows the RECORD and not the call:
// one call serves both idioms and only the synchronous allocation is acknowledged.
EXPECT_FALSE(MGPipeResourceRespecifyNeedsAck(mutableStore));
EXPECT_TRUE(MGPipeResourceRespecifyNeedsAck(immutableStore));
MGPipeApplyResourceSubData(BufferWrite(res, 0, 64), bytes);
EXPECT_EQ(RecordOf(res.Slot).Serial, 3u);
MGPipeApplyBufferSubDataResident(BufferWrite(res, 64, 64), bytes);
EXPECT_EQ(RecordOf(res.Slot).Serial, 4u);
MGPipeApplyResourceFlushRange(MGPFlushRange{res, 0, 64, 0, 0}, bytes);
EXPECT_EQ(RecordOf(res.Slot).Serial, 5u);
// Neither of these two changes the store's contents, so neither may tell the twin its
// memo is stale and buy a re-upload of what it just read.
MGPipeApplyResourceReadback(MGPReadback{res, 0, 512});
EXPECT_EQ(RecordOf(res.Slot).Serial, 5u);
MGPipeApplyMapPersistent(BufferHandle(res), 512, bytes);
EXPECT_EQ(RecordOf(res.Slot).Serial, 5u);
#endif
}
// A destroy drops the record and keeps the generation, because the CLIENT allocator owns
// the bump and takes it on the next handout of the slot. What the destroyed handle names
// afterwards is nothing at all - including after the slot has been handed out again, which
// is the ABA shape a raw address cannot express.
TEST(ResourceEmit, ADestroyDropsTheRecordAndAStaleGenerationResolvesToNothing) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
#else
ApplierGuard guard;
const MGPipeHandle first{7, 3};
MGPipeApplyResourceCreate(BufferDesc(first, 0, 41));
MGPipeApplyResourceRespecify(BufferDesc(first, 256, 41), nullptr);
ASSERT_EQ(RecordOf(first.Slot).Serial, 1u);
MGPipeApplyResourceDestroy(BufferHandle(first));
EXPECT_FALSE(RecordOf(first.Slot).Live);
EXPECT_EQ(RecordOf(first.Slot).Gen, first.Gen) << "the generation is the client's to bump";
EXPECT_EQ(RecordOf(first.Slot).Desc.Width, 0u) << "a stale read of a destroyed slot must "
"find nothing, not the old extent";
EXPECT_EQ(RecordOf(first.Slot).Serial, 0u);
// The dead handle now resolves to nothing, and a mutation on it is dropped rather than
// applied to whatever is at that slot.
MGPipeApplyResourceRespecify(BufferDesc(first, 4096, 41), nullptr);
MGPipeApplyResourceSubData(BufferWrite(first, 0, 16), nullptr);
EXPECT_FALSE(RecordOf(first.Slot).Live);
EXPECT_EQ(RecordOf(first.Slot).Desc.Width, 0u);
EXPECT_EQ(RecordOf(first.Slot).Serial, 0u);
// The same slot at the next generation is a DIFFERENT resource and starts over.
const MGPipeHandle second{7, 4};
MGPipeApplyResourceCreate(BufferDesc(second, 0, 99));
EXPECT_TRUE(RecordOf(second.Slot).Live);
EXPECT_EQ(RecordOf(second.Slot).Gen, second.Gen);
EXPECT_EQ(RecordOf(second.Slot).Serial, 0u);
EXPECT_EQ(RecordOf(second.Slot).Desc.GlNameForDiag, 99u);
// And the predecessor's handle still resolves to nothing OVER the live record - the
// generation compare is what stops a buffer at a recycled address from inheriting its
// predecessor's calls.
MGPipeApplyResourceRespecify(BufferDesc(first, 4096, 41), nullptr);
EXPECT_EQ(RecordOf(second.Slot).Desc.Width, 0u);
EXPECT_EQ(RecordOf(second.Slot).Desc.GlNameForDiag, 99u);
EXPECT_EQ(RecordOf(second.Slot).Serial, 0u);
#endif
}
// HasLiveHostWrites is ALWAYS false in this phase and is written by nobody: it exists so
// the phase that pushes persistent-mapped host writes can set it with no new record kind,
// and a verify build refuses to let a producer land under it unannounced. This case walks
// every path this phase has and pins that none of them is one.
TEST(ResourceEmit, NoResourcePathInThisPhaseLeavesHostWritesLive) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
#else
ApplierGuard guard;
const MGPipeHandle res{2, 1};
const Uint8 bytes[64] = {};
MGPipeApplyResourceCreate(BufferDesc(res, 0, 5));
EXPECT_FALSE(RecordOf(res.Slot).HasLiveHostWrites) << "resource_create";
MGPipeApplyResourceRespecify(BufferDesc(res, 256, 5), bytes);
EXPECT_FALSE(RecordOf(res.Slot).HasLiveHostWrites) << "resource_respecify";
MGPipeApplyResourceSubData(BufferWrite(res, 0, 64), bytes);
EXPECT_FALSE(RecordOf(res.Slot).HasLiveHostWrites) << "resource_subdata";
MGPipeApplyBufferSubDataResident(BufferWrite(res, 64, 64), bytes);
EXPECT_FALSE(RecordOf(res.Slot).HasLiveHostWrites) << "buffer_subdata_resident";
MGPipeApplyResourceFlushRange(MGPFlushRange{res, 0, 64, 0, 0}, bytes);
EXPECT_FALSE(RecordOf(res.Slot).HasLiveHostWrites) << "resource_flush_range";
MGPipeApplyResourceReadback(MGPReadback{res, 0, 256});
EXPECT_FALSE(RecordOf(res.Slot).HasLiveHostWrites) << "resource_readback";
// The persistent map is the one that WOULD set it in a later phase, and does not here.
MGPipeApplyMapPersistent(BufferHandle(res), 256, bytes);
EXPECT_FALSE(RecordOf(res.Slot).HasLiveHostWrites) << "map_persistent";
MGPipeApplyUnmapPersistent(BufferHandle(res));
EXPECT_FALSE(RecordOf(res.Slot).HasLiveHostWrites) << "unmap_persistent";
MGPipeApplyResourceDestroy(BufferHandle(res));
EXPECT_FALSE(RecordOf(res.Slot).HasLiveHostWrites) << "resource_destroy";
// A fresh applier carries none of it over either.
MGPipeApplierReset();
EXPECT_TRUE(MGPipeApplier().Resources.empty());
#endif
}
// The buffer half of MGPSubData is a convention over a texture record's box, and
// MGPipeSetSubDataBufferRange is its only encoder. Its two bounds are what the emitter
// splits against, so they are pinned here exactly - one byte on either side of each.
//
// This case is NOT push-gated: the encoding is an inline function of the payload header
// and exists in every build, so pinning it in the pull build too costs nothing and keeps
// the bound honest for the transport that will read it.
TEST(ResourceEmit, TheSubDataRangeEncodingRefusesExactlyAtItsTwoBounds) {
MGPSubData record{};
// The last encodable offset and the last encodable size are ACCEPTED, and both survive
// the round trip through the box.
EXPECT_TRUE(MGPipeSetSubDataBufferRange(record, 0x7FFFFFFFull, 0));
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 0x7FFFFFFFull);
EXPECT_EQ(MGPipeSubDataBufferSize(record), 0u);
EXPECT_TRUE(MGPipeSetSubDataBufferRange(record, 0, 0xFFFFFFFFull));
EXPECT_EQ(MGPipeSubDataBufferOffset(record), 0u);
EXPECT_EQ(MGPipeSubDataBufferSize(record), 0xFFFFFFFFull);
EXPECT_TRUE(MGPipeSetSubDataBufferRange(record, 0x7FFFFFFFull, 0xFFFFFFFFull));
// The box shape the applier's gate holds the record to, written by the encoder itself.
EXPECT_EQ(record.Level, 0u);
EXPECT_EQ(record.RegionCount, 0u);
// One byte past either bound is REFUSED - and the record is left untouched, which is
// what lets the emitter split against the very record it just tried.
MGPSubData untouched = record;
EXPECT_FALSE(MGPipeSetSubDataBufferRange(record, 0x80000000ull, 0));
EXPECT_FALSE(MGPipeSetSubDataBufferRange(record, 0, 0x100000000ull));
EXPECT_FALSE(MGPipeSetSubDataBufferRange(record, 0x80000000ull, 0x100000000ull));
EXPECT_EQ(std::memcmp(&record, &untouched, sizeof(record)), 0)
<< "a refused encoding must not half-write the record";
// A whole-buffer range at the size bound is the shape the splitter's own case will
// start from; the split itself is the client emitter's and is asserted beside it.
EXPECT_TRUE(MGPipeSetSubDataBufferRange(record, 0, 0xFFFFFFFFull));
}
// BEHAVIOUR NEUTRALITY, ASSERTED RATHER THAN ASSUMED. Every dispatch in the family is a
// null check that falls through while no backend has installed a table - which is what
// lets the client half land without changing a single observable - and every one of them
// hands the backend a HANDLE and a payload, never a frontend object.
TEST(ResourceEmit, EveryResourceCallDispatchesByHandleThroughTheInstalledTableOnly) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
#else
ApplierGuard guard;
const MGPipeHandle res{4, 9};
const Uint8 bytes[64] = {};
g_spy = SpyState{};
// With nothing installed the records still move and nothing is called.
MGPipeApplyResourceCreate(BufferDesc(res, 0, 12));
MGPipeApplyResourceRespecify(BufferDesc(res, 256, 12), bytes);
MGPipeApplyResourceSubData(BufferWrite(res, 0, 64), bytes);
MGPipeApplyBufferSubDataResident(BufferWrite(res, 64, 64), bytes);
MGPipeApplyResourceFlushRange(MGPFlushRange{res, 0, 64, 0, 0}, bytes);
MGPipeApplyResourceReadback(MGPReadback{res, 0, 256});
EXPECT_EQ(MGPipeApplyMapPersistent(BufferHandle(res), 256, bytes), nullptr)
<< "an unregistered table declines every acquisition, which is a real answer";
MGPipeApplyUnmapPersistent(BufferHandle(res));
EXPECT_EQ(RecordOf(res.Slot).Serial, 4u);
EXPECT_EQ(g_spy.Creates + g_spy.Respecifies + g_spy.SubDatas + g_spy.Residents + g_spy.Flushes +
g_spy.Readbacks + g_spy.Destroys + g_spy.Maps + g_spy.Unmaps,
0u);
// Installed, every hook is reached exactly once and with this resource's handle.
MGPipeSetResourceOps(&kSpyOps);
const MGPipeHandle other{5, 1};
MGPipeApplyResourceCreate(BufferDesc(other, 0, 13));
EXPECT_EQ(g_spy.Creates, 1u);
EXPECT_EQ(g_spy.LastHandle, other);
MGPipeApplyResourceRespecify(BufferDesc(other, 256, 13), bytes);
EXPECT_EQ(g_spy.Respecifies, 1u);
MGPipeApplyResourceSubData(BufferWrite(other, 0, 64), bytes);
EXPECT_EQ(g_spy.SubDatas, 1u);
MGPipeApplyBufferSubDataResident(BufferWrite(other, 64, 64), bytes);
EXPECT_EQ(g_spy.Residents, 1u);
MGPipeApplyResourceFlushRange(MGPFlushRange{other, 0, 64, 0, 0}, bytes);
EXPECT_EQ(g_spy.Flushes, 1u);
MGPipeApplyResourceReadback(MGPReadback{other, 0, 256});
EXPECT_EQ(g_spy.Readbacks, 1u);
EXPECT_EQ(MGPipeApplyMapPersistent(BufferHandle(other), 256, bytes), &g_spyMapTarget)
<< "the donated pointer is the owner's answer and travels back unchanged";
EXPECT_EQ(g_spy.LastMapSize, 256u);
MGPipeApplyUnmapPersistent(BufferHandle(other));
EXPECT_EQ(g_spy.Unmaps, 1u);
MGPipeApplyResourceDestroy(BufferHandle(other));
EXPECT_EQ(g_spy.Destroys, 1u);
EXPECT_EQ(g_spy.LastHandle, other);
EXPECT_FALSE(RecordOf(other.Slot).Live);
// Uninstalled again - a backend teardown - and the family goes dark without taking the
// applier's records with it.
MGPipeSetResourceOps(nullptr);
MGPipeApplyResourceSubData(BufferWrite(res, 0, 64), bytes);
EXPECT_EQ(g_spy.SubDatas, 1u);
EXPECT_EQ(RecordOf(res.Slot).Serial, 5u);
#endif
}
// map-persistent-roundtrips counts every ACQUISITION ATTEMPT, mint or decline, because
// every one of them needs an answer from the resource owner. Defined as "round trips
// actually taken" it would be 0 by construction in the monolith and could never go red;
// defined this way the number is the same in both modes, is one per storage definition,
// and is assertable today.
TEST(ResourceEmit, MapPersistentCountsEveryAttemptWhetherItMintsOrDeclines) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
#else
ApplierGuard guard;
const MGPipeHandle res{3, 2};
const Uint8 bytes[64] = {};
g_spy = SpyState{};
EXPECT_EQ(MGPipeApplier().MapPersistentRoundtrips, 0u);
MGPipeApplyResourceCreate(BufferDesc(res, 0, 8));
MGPipeApplyResourceRespecify(BufferDesc(res, 256, 8), bytes);
EXPECT_EQ(MGPipeApplier().MapPersistentRoundtrips, 0u) << "a storage definition is not an "
"acquisition";
// Three declines still cost three answers.
for (Uint32 i = 0; i < 3; ++i) EXPECT_EQ(MGPipeApplyMapPersistent(BufferHandle(res), 256, bytes), nullptr);
EXPECT_EQ(MGPipeApplier().MapPersistentRoundtrips, 3u);
// A mint costs the same one.
MGPipeSetResourceOps(&kSpyOps);
EXPECT_EQ(MGPipeApplyMapPersistent(BufferHandle(res), 256, bytes), &g_spyMapTarget);
EXPECT_EQ(MGPipeApplier().MapPersistentRoundtrips, 4u);
EXPECT_EQ(g_spy.Maps, 1u);
// And so does an attempt on a resource the applier does not have: the client asked,
// and asking is what the counter counts.
MGPipeApplyResourceDestroy(BufferHandle(res));
EXPECT_EQ(MGPipeApplyMapPersistent(BufferHandle(res), 256, bytes), nullptr);
EXPECT_EQ(MGPipeApplier().MapPersistentRoundtrips, 5u);
EXPECT_EQ(g_spy.Maps, 1u) << "a refused handle must not reach the backend";
// Per context, like every other member of the applier's state.
MGPipeApplierReset();
EXPECT_EQ(MGPipeApplier().MapPersistentRoundtrips, 0u);
#endif
}
// THE BOUNDS GATE, AND THE REASON IT IS A TRIP WIRE RATHER THAN A DROPPED CALL: a record
// whose range runs past the storage it names would have the BACKEND read or write outside
// a store. That is the one class of fault ARCHITECTURE.md reserves Fatal{ProtocolCorruption}
// for, and the line carries the record's identity so the resource can be named without a
// second run.
//
// A poison or verify build stops the process, so the drive is a forked child there and the
// parent reads SIGABRT and the line out of the log; a shipped push build logs and carries
// on from a defined state, so that build asserts the same line plus the fact that the
// refused write moved nothing.
TEST(ResourceEmit, AWriteOutsideTheDeclaredStorageIsRefusedNamingTheResource) {
#if !MOBILEGL_PIPE_PUSH
GTEST_SKIP() << "MOBILEGL_PIPE_PUSH is off: there is no applier in this build";
#else
ApplierGuard guard;
const MGPipeHandle res{7, 3};
const Uint8 bytes[256] = {};
MGPipeApplyResourceCreate(BufferDesc(res, 0, 41));
MGPipeApplyResourceRespecify(BufferDesc(res, 256, 41), nullptr);
// The positive control first: a write that ends EXACTLY at the declared extent is
// accepted, so the gate is refusing the range and not the arithmetic around it.
MGPipeApplyResourceSubData(BufferWrite(res, 192, 64), bytes);
ASSERT_EQ(RecordOf(res.Slot).Serial, 2u);
// Encoded HERE and not inside the child: a forked child must not run a gtest assertion,
// and BufferWrite carries one.
MGPSubData pastTheEnd{};
pastTheEnd.Res = res;
ASSERT_TRUE(MGPipeSetSubDataBufferRange(pastTheEnd, 200, 64));
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
#if MGTEST_HAVE_FORK
const ChildResult child =
RunInChild([&pastTheEnd, &bytes]() { MGPipeApplyResourceSubData(pastTheEnd, bytes); });
EXPECT_TRUE(DiedOfAbort(child)) << DescribeStatus(child) << "; log: " << child.Log;
EXPECT_NE(child.Log.find("Fatal{ProtocolCorruption} resource_subdata {slot=7, gen=3, glName=41}"),
std::string::npos)
<< "the gate fired without naming the record it refused; log: " << child.Log;
#else
GTEST_SKIP() << "no fork on this platform; the gate's verdict here is std::abort()";
#endif
#else
const std::string before = ReadLog();
MGPipeApplyResourceSubData(pastTheEnd, bytes);
EXPECT_EQ(RecordOf(res.Slot).Serial, 2u) << "a refused write must not move the serial";
EXPECT_NE(ReadLog().substr(before.size()).find("ProtocolCorruption resource_subdata {slot=7, gen=3, "
"glName=41}"),
std::string::npos)
<< "the gate refused the write without saying which record it was";
#endif
#endif
}
} // namespace