[Test] (P11 B2): T0 protocol on the host - the Offer inbox (late, out of order, stale, missing) over both aux channels, the whole T0 path on a stand-in platform, A1's two handshake-Fatal cases rewritten as named fallbacks, revision 4 refused by name, AdoptTier0 lanes on the shared-segment arms and the allow switch through spawn

This commit is contained in:
2026-09-29 17:50:50 -04:00
parent 7cde55b1c1
commit 2522f1803c
10 changed files with 723 additions and 79 deletions
+54 -13
View File
@@ -2483,6 +2483,41 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
)
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
# P11 B2 (CONTRACT-P11 B2): THE SHARED-SEGMENT ARMS WITH MOBILEGL_IPC_ADOPT_TIER=0, which A1 made
# die at the handshake and B2 turned into an ASK. A host has no AHardwareBuffer, so the server
# refuses T0 by name at its first native bind, publishes no kCapAdoptT0, and the client falls
# back to T2 with one named line at its first map_persistent: every case green, the arena
# emulated (SplitLogPaths declares it), ASessionThatCannotRunT0FallsBackToT2OnceByName reading
# the two lines. `.AdoptT0Disallowed.` is the same on the spawn arm with the server's allow switch
# off - which also proves MOBILEGL_IPC_ALLOW_ADOPT_T0 survives ServerSpawn's environment scrub.
foreach(mglT0Arm IN ITEMS Split Spawn)
string(TOUPPER "${mglT0Arm}" mglT0ArmUpper)
string(TOLOWER "${mglT0Arm}" mglT0ArmLower)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.${mglT0Arm}.AdoptTier0."
TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}"
TEST_FILTER "LargeArenaAdoptionScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-${mglT0ArmLower}"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_${mglT0ArmUpper}_ENVIRONMENT}\;MOBILEGL_IPC_ADOPT_TIER=0\;MGITEST_ADOPT_T0_FALLBACK=no AHardwareBuffer"
)
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
endforeach()
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Spawn.AdoptT0Disallowed."
TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}"
TEST_FILTER "LargeArenaAdoptionScenario.ASessionThatCannotRunT0FallsBackToT2OnceByName:LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-spawn"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_SPAWN_ENVIRONMENT}\;MOBILEGL_IPC_ADOPT_TIER=0\;MOBILEGL_IPC_ALLOW_ADOPT_T0=0\;MGITEST_ADOPT_T0_FALLBACK=disallowed"
)
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
# ---- P3b/P4b espryt D1 slice 1: THE ORPHANED XFB CAPTURE TARGET -------------------------
#
@@ -3864,21 +3899,27 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MGITEST_PERSISTENT_MAP_ARM=emulated\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-arena${mglItestArmLogTag}-${arenaCase}.log")
endforeach()
# P11 A1 (CONTRACT-P11 §1): the tcp arm with MOBILEGL_IPC_ADOPT_TIER=0 - refused by name at
# the client's handshake, T2, the scenario green. TCP ONLY by construction (over shared
# segments the knob dies at the handshake); spawn_lane_parity.py names the tail in
# MAGMA_TCP_ONLY.
# P11 A1 + B2 (CONTRACT-P11 §1, B2): MOBILEGL_IPC_ADOPT_TIER=0 on every arm, one case set.
# On tcp a stream refuses the tier by name at the client's handshake (A1, the stream case
# reads the line); over shared segments (split, spawn) the knob ASKS T0, a host has no
# AHardwareBuffer, and the session falls back to T2 by name (B2, the fallback case reads
# both lines). Each arm's marker makes the other arm's case skip, so the three arms carry
# the same names and spawn_lane_parity.py compares them like any other lane.
if(mglItestArm STREQUAL "Tcp")
foreach(arenaCase IN LISTS MGL_ITEST_ARENA_CASES ITEMS AStreamSessionRefusesTheAdoptTierOnceByName)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.Tcp.AdoptTier0."
TEST_FILTER "LargeArenaAdoptionScenario.${arenaCase}"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT}
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MOBILEGL_IPC_ADOPT_TIER=0\;MGITEST_ADOPT_TIER_REFUSAL=T0\;MGITEST_PERSISTENT_MAP_ARM=emulated\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-adopt-tier0-tcp-${arenaCase}.log")
endforeach()
set(mglItestAdoptTier0Env "MGITEST_ADOPT_TIER_REFUSAL=T0")
else()
set(mglItestAdoptTier0Env "MGITEST_ADOPT_T0_FALLBACK=no AHardwareBuffer")
endif()
foreach(arenaCase IN LISTS MGL_ITEST_ARENA_CASES ITEMS AStreamSessionRefusesTheAdoptTierOnceByName
ASessionThatCannotRunT0FallsBackToT2OnceByName)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.AdoptTier0."
TEST_FILTER "LargeArenaAdoptionScenario.${arenaCase}"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT}
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MOBILEGL_IPC_ADOPT_TIER=0\;${mglItestAdoptTier0Env}\;MGITEST_PERSISTENT_MAP_ARM=emulated\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p11-adopt-tier0${mglItestArmLogTag}-${arenaCase}.log")
endforeach()
endforeach()
endif()
@@ -99,6 +99,9 @@ void main() { word = 0xC0FFEEu; }
// P11 A1: set (to the tier the lane names, "T0") only by the `.AdoptTier0.` tcp entries,
// which also set MOBILEGL_IPC_ADOPT_TIER=0; a harness marker, never read by the library.
constexpr const char* kAdoptTierRefusalMarker = "MGITEST_ADOPT_TIER_REFUSAL";
// P11 B2: set by the shared-segment `.AdoptTier0.` / `.AdoptT0Disallowed.` entries to the reason word the
// server gives for not granting T0 ("no AHardwareBuffer" on a host, "disallowed" with its allow switch off).
constexpr const char* kAdoptT0FallbackMarker = "MGITEST_ADOPT_T0_FALLBACK";
// Draws issued against the arena inside the counted window. One definition, many draws:
// "one per definition" (1) and "one per draw" (kDrawsInTheWindow) have to be different
// numbers or the assertion cannot tell them apart.
@@ -619,4 +622,52 @@ void main() { word = 0xC0FFEEu; }
EXPECT_EQ(log.find("Fatal{UnimplementedAdoptTier"), std::string::npos) << log;
}
// P11 B2 (MG_Remote/CONTRACT-P11.md B2): A SHARED-SEGMENT SESSION THAT ASKS T0 AND CANNOT RUN IT.
// MOBILEGL_IPC_ADOPT_TIER=0 over shared segments asks T0 in the Hello (A1 made it die at the
// handshake; ruling ID-P11-14 made it this). The lane's server cannot grant it - a host has no
// AHardwareBuffer, or `.AdoptT0Disallowed.` turned its allow switch off - so it refuses by name
// at its first native bind, publishes no kCapAdoptT0, and the client falls back to T2 with ONE
// named line at its first map_persistent. Never a Fatal; the other cases in the lane are green
// and the arena is emulated. Everywhere else it skips.
TEST_F(LargeArenaAdoptionScenario, ASessionThatCannotRunT0FallsBackToT2OnceByName) {
if (!Ready() || IsSkipped()) return;
const char* why = std::getenv(kAdoptT0FallbackMarker);
if (why == nullptr || why[0] == '\0') {
GTEST_SKIP() << "runs only in the shared-segment `.AdoptTier0.` / `.AdoptT0Disallowed.` entries, "
"which set MOBILEGL_IPC_ADOPT_TIER=0 and " << kAdoptT0FallbackMarker;
}
if (PipeStatsWindow::LibraryLogPath().empty()) {
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the two logs are where the "
"fallback is written";
}
// A frame of the arena's work first: the fallback is only worth reading from a session that
// then ran on T2 - and the client decides at its first map_persistent, which SetUp's
// definition of the arena has already made.
UploadQuad(0.f, 1.f, 0.f);
DrawQuad();
EXPECT_GT(CenterPixel()[1], 200) << "the session that fell back to T2 did not render";
EXPECT_EQ(FirstGLError(), 0u);
Gl().EndFrame();
const auto countOf = [](const std::string& text, const std::string& needle) {
std::size_t n = 0;
for (auto at = text.find(needle); at != std::string::npos; at = text.find(needle, at + 1)) ++n;
return n;
};
const std::string client = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::LibraryLogPath());
const std::string server = PipeStatsWindow::ReadServerLogSince(PipeStatsWindow::LogMark{});
const std::string clientLine = "Refuse{AdoptT0Unavailable, \"no kCapAdoptT0\"}";
const std::string serverLine = std::string("Refuse{AdoptT0Unavailable, \"") + why + "\"}";
RecordProperty("t0_client_fallbacks", static_cast<int>(countOf(client, clientLine)));
RecordProperty("t0_server_refusals", static_cast<int>(countOf(server, serverLine)));
EXPECT_EQ(countOf(client, clientLine), 1u)
<< "the client names a T0 it asked for and cannot run exactly once, at its first map_persistent ("
<< clientLine << "), in " << PipeStatsWindow::LibraryLogPath();
EXPECT_EQ(countOf(server, serverLine), 1u)
<< "the server names why it did not grant T0 exactly once, at its first native bind (" << serverLine
<< "), in " << PipeStatsWindow::ServerLibraryLogPath();
EXPECT_EQ(client.find("Fatal{"), std::string::npos) << client;
EXPECT_EQ(server.find("Fatal{"), std::string::npos) << server;
}
} // namespace MGITest
+14 -10
View File
@@ -538,13 +538,14 @@ TEST_F(SplitBufferSet, UnderSplitTheWritebackClearsThePendingFlagAndNotTheReques
EXPECT_FALSE(buffer->HasOutstandingGpuWrite());
}
// R-6's tier gate in P11 A1's shape (CONTRACT-P11 §1). T2 is the only implemented tier, and 0 / 1
// are SETTLED AT THE HANDSHAKE by the data plane rather than discovered at the first
// map_persistent: a stream refuses them by name and runs T2 on either side, a client over shared
// segments dies there by name, a server over shared segments serves T2. The at-use check trusts a
// settled session and still dies by name for a map_persistent no handshake settled. The session
// halves are RemoteClientControls.AdoptTier*OverSharedSegments* and AdoptTierStreamTest.
TEST_F(SplitBufferSet, OnlyAdoptTierTwoIsImplemented) {
// The tier gate in P11 B2's shape (CONTRACT-P11 §1 and B2). 0 / 1 are SETTLED AT THE HANDSHAKE by
// the data plane rather than discovered at the first map_persistent: a stream refuses them by name
// and runs T2 on either side; over shared segments a client with 0 ASKS T0 (0), with 1 is refused
// by name and asks T2, and a server serves by the client's ask (its own knob answers 2). None of
// them dies any more (ID-P11-14). The at-use check trusts a settled session and still dies by
// name for a map_persistent no handshake settled. The session halves are RemoteClientAdoptT0.*
// and AdoptTierStreamTest.
TEST_F(SplitBufferSet, TheAdoptTierIsSettledAtTheHandshakeAndNeverFatal) {
namespace Tier = MG_Remote::Transport;
EXPECT_TRUE(MG_Remote::Client::AdoptTierIsEmulate());
for (const Uint32 tier : {0u, 1u}) {
@@ -552,8 +553,11 @@ TEST_F(SplitBufferSet, OnlyAdoptTierTwoIsImplemented) {
MG_Config::Ipc.AdoptTier = tier;
Tier::ForgetAdoptTierSettlementForTest();
EXPECT_DEATH(MG_Remote::Client::AdoptTierIsEmulate(), "UnimplementedAdoptTier.*no handshake settled");
EXPECT_DEATH(Tier::SettleAdoptTierAtHandshake(false, Tier::AdoptTierSide::Client),
"UnimplementedAdoptTier.*refused at the handshake over shared segments");
Tier::ForgetAdoptTierSettlementForTest();
EXPECT_EQ(Tier::SettleAdoptTierAtHandshake(false, Tier::AdoptTierSide::Client), tier == 0 ? 0u : 2u);
EXPECT_EQ(Tier::AdoptTierAskFor(false), tier == 0 ? 0u : 2u);
EXPECT_EQ(Tier::AdoptTierAskFor(true), 2u);
EXPECT_TRUE(MG_Remote::Client::AdoptTierIsEmulate());
for (const auto side : {Tier::AdoptTierSide::Client, Tier::AdoptTierSide::Server}) {
Tier::ForgetAdoptTierSettlementForTest();
EXPECT_EQ(Tier::SettleAdoptTierAtHandshake(true, side), 2u);
@@ -777,7 +781,7 @@ TEST(SplitBufferSet, BothEdgesOfAWriteMapPublishOneStateRecord) { MGL_SPLIT_ONLY
TEST(SplitBufferSet, AZeroBlockSizeTurnsThePushOffRatherThanMakingItUnlimited) { MGL_SPLIT_ONLY_OR_SKIP(); }
TEST(SplitBufferSet, ADestroyedBufferLeavesTheSet) { MGL_SPLIT_ONLY_OR_SKIP(); }
TEST(SplitBufferSet, UnderSplitTheWritebackClearsThePendingFlagAndNotTheRequest) { MGL_SPLIT_ONLY_OR_SKIP(); }
TEST(SplitBufferSet, OnlyAdoptTierTwoIsImplemented) { MGL_SPLIT_ONLY_OR_SKIP(); }
TEST(SplitBufferSet, TheAdoptTierIsSettledAtTheHandshakeAndNeverFatal) { MGL_SPLIT_ONLY_OR_SKIP(); }
TEST(SplitBufferSet, ASubPageMapIsOneProtectedPageAndAWriteThroughItFaults) { MGL_SPLIT_ONLY_OR_SKIP(); }
TEST(SplitBufferSet, AShadowThatFailsThePageGranularTestKeepsTheInwardAlignmentAndItsEdges) {
MGL_SPLIT_ONLY_OR_SKIP();
+200
View File
@@ -0,0 +1,200 @@
// MobileGL - MobileGL/MG_Test/Wire/AdoptInboxTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P11 B2 (MG_Remote/CONTRACT-P11.md B2). Server::AdoptInbox - T0's Offers matched to their
// map_persistent records BY SEQ, never by arrival order - over both aux channels the product has:
// the in-process queue (inproc) and a real socket pair with SCM_RIGHTS (fork spawn / unix / fd:).
//
// Every case proves WHICH descriptor came back, not just that one did: each Offer's descriptor is
// a pipe whose far end carries the Offer's own seq, so a mix-up reads the wrong number.
#include <MG_Remote/Server/AdoptInbox.h>
#include <MG_Remote/Transport/AdoptT0.h>
#include <MG_Remote/Transport/InProcessTransport.h>
#include <MG_Remote/Transport/SocketTransport.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstdint>
#include <memory>
#include <string>
#include <thread>
#include <unistd.h>
using namespace MobileGL;
using namespace MobileGL::MG_Remote;
namespace T0 = MobileGL::MG_Remote::Transport::AdoptT0;
namespace {
struct Channel {
std::unique_ptr<Transport::ITransport> client;
std::unique_ptr<Transport::ITransport> server;
};
Channel MakeChannel(bool socket) {
Channel c;
if (socket) {
std::unique_ptr<Transport::SocketTransport> a, b;
EXPECT_EQ(Transport::SocketTransport::CreatePair(a, b), MOBILEGL_OK);
c.client = std::move(a);
c.server = std::move(b);
} else {
std::unique_ptr<Transport::InProcessTransport> a, b;
Transport::InProcessTransport::CreatePair(a, b);
c.client = std::move(a);
c.server = std::move(b);
}
return c;
}
// An Offer for `seq` whose descriptor is a pipe's read end; the pipe carries `seq` itself.
void SendOffer(Transport::ITransport& client, std::uint64_t seq, std::uint32_t flags = T0::kOfferHasBuffer) {
int p[2] = {-1, -1};
ASSERT_EQ(::pipe(p), 0);
ASSERT_EQ(::write(p[1], &seq, sizeof(seq)), static_cast<ssize_t>(sizeof(seq)));
::close(p[1]);
T0::Offer offer{};
offer.magic = T0::kOfferMagic;
offer.version = T0::kOfferVersion;
offer.seq = seq;
offer.size = 4096;
offer.flags = flags;
ASSERT_EQ(client.ShareFd(p[0], MobileGLByteSpan{&offer, sizeof(offer)}), MOBILEGL_OK);
::close(p[0]);
}
// The seq the descriptor's pipe carries, and closes it.
std::uint64_t SeqOf(int fd) {
std::uint64_t seq = ~0ull;
EXPECT_EQ(::read(fd, &seq, sizeof(seq)), static_cast<ssize_t>(sizeof(seq)));
::close(fd);
return seq;
}
class AdoptInboxTest : public ::testing::TestWithParam<bool> {};
} // namespace
// In order: each Take finds its own Offer.
TEST_P(AdoptInboxTest, OffersInOrderAreTakenByTheirOwnRecords) {
Channel c = MakeChannel(GetParam());
Server::AdoptInbox inbox;
inbox.Attach(c.server.get());
SendOffer(*c.client, 10);
SendOffer(*c.client, 11);
for (std::uint64_t seq : {10ull, 11ull}) {
int fd = -1;
T0::Offer offer{};
std::string why;
ASSERT_EQ(inbox.Take(seq, 1000, &fd, &offer, why), Server::AdoptInbox::Outcome::Taken) << why;
EXPECT_EQ(offer.seq, seq);
EXPECT_EQ(SeqOf(fd), seq);
}
EXPECT_EQ(inbox.Kept(), 0u);
}
// OUT OF ORDER: the Offer for the later record arrives first. It is kept for that record, the
// earlier record still gets its own, and the later one is then served from what was kept.
TEST_P(AdoptInboxTest, AnOfferForALaterRecordIsKeptForItAndDoesNotAnswerAnEarlierOne) {
Channel c = MakeChannel(GetParam());
Server::AdoptInbox inbox;
inbox.Attach(c.server.get());
SendOffer(*c.client, 21);
SendOffer(*c.client, 20);
int fd = -1;
T0::Offer offer{};
std::string why;
ASSERT_EQ(inbox.Take(20, 1000, &fd, &offer, why), Server::AdoptInbox::Outcome::Taken) << why;
EXPECT_EQ(SeqOf(fd), 20u);
EXPECT_EQ(inbox.Kept(), 1u);
ASSERT_EQ(inbox.Take(21, 0, &fd, &offer, why), Server::AdoptInbox::Outcome::Taken) << why;
EXPECT_EQ(SeqOf(fd), 21u);
EXPECT_EQ(inbox.Kept(), 0u);
}
// LATE: the record is being waited for when its Offer arrives.
TEST_P(AdoptInboxTest, AnOfferThatArrivesDuringTheWaitIsTaken) {
Channel c = MakeChannel(GetParam());
Server::AdoptInbox inbox;
inbox.Attach(c.server.get());
std::thread late([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
SendOffer(*c.client, 30);
});
int fd = -1;
T0::Offer offer{};
std::string why;
const auto start = std::chrono::steady_clock::now();
const auto outcome = inbox.Take(30, 2000, &fd, &offer, why);
const auto waited = std::chrono::steady_clock::now() - start;
late.join();
ASSERT_EQ(outcome, Server::AdoptInbox::Outcome::Taken) << why;
EXPECT_EQ(SeqOf(fd), 30u);
EXPECT_GE(waited, std::chrono::milliseconds(150));
}
// STALE: an Offer for a record that was already answered (its wait timed out) is closed and
// counted, and does not answer the record being waited for.
TEST_P(AdoptInboxTest, AnOfferForAnEarlierRecordIsStaleAndClosed) {
Channel c = MakeChannel(GetParam());
Server::AdoptInbox inbox;
inbox.Attach(c.server.get());
SendOffer(*c.client, 40);
SendOffer(*c.client, 41);
int fd = -1;
T0::Offer offer{};
std::string why;
ASSERT_EQ(inbox.Take(41, 1000, &fd, &offer, why), Server::AdoptInbox::Outcome::Taken) << why;
EXPECT_EQ(SeqOf(fd), 41u);
EXPECT_EQ(inbox.StaleOffers(), 1u);
}
// MISSING: nothing comes - a bounded TimedOut with a reason, never a hang.
TEST_P(AdoptInboxTest, AnOfferThatNeverComesTimesOutWithinItsBound) {
Channel c = MakeChannel(GetParam());
Server::AdoptInbox inbox;
inbox.Attach(c.server.get());
int fd = -1;
T0::Offer offer{};
std::string why;
const auto start = std::chrono::steady_clock::now();
EXPECT_EQ(inbox.Take(50, 300, &fd, &offer, why), Server::AdoptInbox::Outcome::TimedOut);
const auto waited = std::chrono::steady_clock::now() - start;
EXPECT_EQ(fd, -1);
EXPECT_NE(why.find("no store arrived"), std::string::npos) << why;
EXPECT_LT(waited, std::chrono::milliseconds(2000));
}
// A descriptor whose sideband is not an Offer (another sender on the channel) is closed and
// counted, and the real Offer behind it still answers.
TEST_P(AdoptInboxTest, ADescriptorThatIsNotAnOfferIsSkipped) {
Channel c = MakeChannel(GetParam());
Server::AdoptInbox inbox;
inbox.Attach(c.server.get());
int p[2] = {-1, -1};
ASSERT_EQ(::pipe(p), 0);
const std::uint32_t junk = 7;
ASSERT_EQ(c.client->ShareFd(p[0], MobileGLByteSpan{&junk, sizeof(junk)}), MOBILEGL_OK);
::close(p[0]);
::close(p[1]);
SendOffer(*c.client, 60);
int fd = -1;
T0::Offer offer{};
std::string why;
ASSERT_EQ(inbox.Take(60, 1000, &fd, &offer, why), Server::AdoptInbox::Outcome::Taken) << why;
EXPECT_EQ(SeqOf(fd), 60u);
EXPECT_EQ(inbox.MalformedOffers(), 1u);
}
INSTANTIATE_TEST_SUITE_P(Channels, AdoptInboxTest, ::testing::Values(false, true),
[](const ::testing::TestParamInfo<bool>& info) {
return info.param ? std::string("socket") : std::string("inprocess");
});
+4
View File
@@ -36,6 +36,10 @@ if (NOT WIN32)
# P11 PAIR: the unix supervisors' pairing of a client's control and aux connections by the
# nonce each presents first (Server/PairAcceptor), over real unix and TCP listeners.
list(APPEND MOBILEGL_WIRE_TESTS PairAcceptorTest)
# P11 B2: T0's Offers matched to their map_persistent records by seq (Server/AdoptInbox) over
# the in-process queue and a real SCM_RIGHTS socket pair: in order, out of order, late, stale,
# missing (bounded), not-an-Offer.
list(APPEND MOBILEGL_WIRE_TESTS AdoptInboxTest)
# P9 W3: the graded log forward - the level mapping both ways, the logger's level reaching the
# forwarder, and the channel against a transport whose writes can be held (and a real socket
# pair nobody reads). POSIX for the socket pair.
@@ -0,0 +1,330 @@
// P11 B2 (MG_Remote/CONTRACT-P11.md B2): T0'S PROTOCOL ON THE HOST, over a live inproc session.
//
// A host has no AHardwareBuffer, so what these prove is the protocol, the negotiation and the
// fallback - never GPU correctness, which is the device's (B2's device gates). Two halves:
// * the real host platform: every T0 ask falls back to T2 by name (no AHB, the allow switch,
// knob 1), and never dies - A1's handshake Fatal for SharedSegments + 0/1 is gone;
// * a STAND-IN platform (a memfd for the AHB, AdoptT0::SetPlatformForTest) plus a stand-in
// importer (MGPipeResourceOps::ImportExternal): the whole T0 path runs - the grant, the caps
// bit, the client's store, the Offer on the real aux channel matched by seq, the import - and
// the store lands with the client's bytes, including when its Offer comes after its record;
// a lost Offer is a named DECLINED within the bounded wait, not a hang.
#if MGTEST_HAVE_FORK
#include <MG_Remote/Transport/AdoptT0.h>
#include <MG_Remote/Transport/AdoptTier.h>
#include <MG_Remote/Transport/FdPassing.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/socket.h>
namespace {
namespace T0 = MobileGL::MG_Remote::Transport::AdoptT0;
constexpr Uint64 kT0Bytes = 64 * 1024;
// ---- the stand-in AHB: a memfd, mapped shared, reference-counted -------------------------
struct FakeAhb {
int fd = -1;
void* map = nullptr;
Uint64 size = 0;
std::atomic<int> refs{1};
};
void FakeDrop(FakeAhb* f) {
if (f != nullptr && --f->refs == 0) {
if (f->map != nullptr) ::munmap(f->map, f->size);
if (f->fd >= 0) ::close(f->fd);
delete f;
}
}
bool FakeHasAhb() { return true; }
bool FakeAllocate(std::uint64_t size, const void* seed, T0::HeldStore& out, std::string& why) {
auto* f = new FakeAhb;
f->fd = static_cast<int>(::syscall(SYS_memfd_create, "t0-fake", 1u /*MFD_CLOEXEC*/));
if (f->fd < 0 || ::ftruncate(f->fd, static_cast<off_t>(size)) != 0) {
why = "memfd";
FakeDrop(f);
return false;
}
f->size = size;
f->map = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, f->fd, 0);
if (f->map == MAP_FAILED) {
f->map = nullptr;
why = "mmap";
FakeDrop(f);
return false;
}
if (seed != nullptr) std::memcpy(f->map, seed, size);
out.ahb = f;
out.ptr = f->map;
out.size = size;
return true;
}
int FakeMakeHop(const T0::HeldStore* store, std::string& why) {
int pair[2] = {-1, -1};
if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) != 0) {
why = "socketpair";
return -1;
}
if (store != nullptr && store->ahb != nullptr) {
auto* f = static_cast<FakeAhb*>(store->ahb);
const Uint64 size = f->size;
if (Transport::FdPassing::SendFd(pair[0], f->fd, MobileGLByteSpan{&size, sizeof(size)}) != MOBILEGL_OK) {
why = "send";
::close(pair[0]);
::close(pair[1]);
return -1;
}
}
::close(pair[0]);
return pair[1];
}
void FakeReleaseHeld(T0::HeldStore& store) {
FakeDrop(static_cast<FakeAhb*>(store.ahb));
store = T0::HeldStore{};
}
bool FakeReceive(int hopFd, std::uint64_t size, void** outAhb, std::string& why) {
*outAhb = nullptr;
int fd = -1;
Uint8 sideband[Transport::FdPassing::kMaxSidebandBytes] = {};
std::uint64_t sidebandSize = 0;
if (Transport::FdPassing::ReceiveFd(hopFd, &fd, MobileGLMutableByteSpan{sideband, sizeof(sideband)},
&sidebandSize, 1000) != MOBILEGL_OK) {
why = "no stand-in handle on the hop";
return false;
}
auto* f = new FakeAhb;
f->fd = fd;
f->size = size;
f->map = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (f->map == MAP_FAILED) {
f->map = nullptr;
FakeDrop(f);
why = "mmap";
return false;
}
*outAhb = f;
return true;
}
void FakeAcquire(void* ahb) { ++static_cast<FakeAhb*>(ahb)->refs; }
void FakeRelease(void* ahb) { FakeDrop(static_cast<FakeAhb*>(ahb)); }
const T0::PlatformForTest kFakePlatform{&FakeHasAhb, &FakeAllocate, &FakeMakeHop, &FakeReleaseHeld,
&FakeReceive, &FakeAcquire, &FakeRelease};
// ---- the stand-in importer: records what it was handed, keeps its own reference ----------
struct FakeImports {
MGPipeHandle handle{};
Uint64 size = 0;
Uint64 count = 0;
std::vector<Uint8> bytes;
void* held = nullptr;
};
FakeImports g_fakeImports;
Bool FakeImportExternal(MGPipeHandle res, void* ahb, Uint64 size) {
auto* f = static_cast<FakeAhb*>(ahb);
g_fakeImports.handle = res;
g_fakeImports.size = size;
++g_fakeImports.count;
const auto* first = static_cast<const Uint8*>(f->map);
g_fakeImports.bytes.assign(first, first + size);
T0::AcquireImported(ahb);
g_fakeImports.held = ahb;
return true;
}
Bool FakeSelfTest(char* why, Uint64 whyBytes) {
std::snprintf(why, static_cast<size_t>(whyBytes), "stand-in platform (host protocol test, no GPU)");
return true;
}
MGPipeResourceOps MakeFakeOps() {
MGPipeResourceOps ops{};
ops.ImportExternal = &FakeImportExternal;
ops.SelfTestExternal = &FakeSelfTest;
return ops;
}
MGPipeResourceOps g_fakeOps = MakeFakeOps();
// What the server's first native bind does for T0, on the apply thread like the real one: the
// grant (and its POST), then the caps that bind republishes - which the client's make-current
// adopts, as here.
void GrantAtFirstBind() {
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
Srv::ServerSessionInstance().SettleAdoptT0AtBind();
return Srv::ServerSessionInstance().PublishCapsSnapshot();
}, nullptr) != MOBILEGL_OK) ::_exit(120);
(void)ClientSessionInstance().PumpControlPlane();
}
MGPHandleOnly MakeBufferStore(Uint32 slot, Uint64 bytes) {
MGPResourceDesc desc{};
desc.Resource = MGPipeHandle{slot, 1};
desc.Target = MG_Pipe::kMGPipeResourceTargetBuffer;
desc.Width = static_cast<Uint32>(bytes);
desc.Height = desc.Depth = 1;
if (!MGPipeRouteResourceCreate(desc)) ::_exit(121);
(void)MGPipeRouteResourceRespecify(desc, nullptr);
MGPHandleOnly handle{};
handle.Handle = desc.Resource;
handle.Kind = static_cast<Uint32>(MGPipeKind::Buffer);
return handle;
}
std::vector<Uint8> Pattern(Uint64 bytes, Uint8 salt) {
std::vector<Uint8> out(bytes);
for (Uint64 i = 0; i < bytes; ++i) out[i] = static_cast<Uint8>((i * 131u + salt) & 0xFF);
return out;
}
size_t CountOf(const std::string& log, const std::string& needle) {
size_t n = 0;
for (size_t at = log.find(needle); at != std::string::npos; at = log.find(needle, at + 1)) ++n;
return n;
}
// The grant with the stand-ins, the store, and the map_persistent: returns the adopted pointer.
void* AdoptOneStore(Uint8 salt, std::vector<Uint8>& seed) {
MG_Config::Ipc.AdoptTier = 0;
T0::SetPlatformForTest(&kFakePlatform);
MG_Pipe::MGPipeSetResourceOps(&g_fakeOps);
StartControlSession();
GrantAtFirstBind();
seed = Pattern(kT0Bytes, salt);
const MGPHandleOnly handle = MakeBufferStore(5, kT0Bytes);
return MGPipeRouteMapPersistent(handle, kT0Bytes, seed.data());
}
} // namespace
// A1's "SharedSegments + 0 dies at the handshake" is GONE (ID-P11-14): knob 0 over shared segments
// ASKS T0 in the Hello, and a server that does not grant it (here: no bind ever ran, so no
// kCapAdoptT0) leaves the session on T2 with ONE named client line at the first map_persistent.
// RED-ONCE: restore the client's SessionFail in SettleAdoptTierAtHandshake -> the child dies
// before 'S'.
TEST(RemoteClientAdoptT0, KnobZeroOverSharedSegmentsAsksT0AndFallsBackToT2ByNameWithoutAGrant) {
const auto child = RunInChild([] {
MG_Config::Ipc.AdoptTier = 0;
StartControlSession();
if (!Srv::ServerSessionInstance().AdoptT0Asked()) ::_exit(130); // the Hello asked T0
MGPHandleOnly handle{};
handle.Kind = static_cast<Uint32>(MGPipeKind::Buffer);
if (MGPipeRouteMapPersistent(handle, 64, nullptr) != nullptr) ::_exit(131);
if (MGPipeRouteMapPersistent(handle, 64, nullptr) != nullptr) ::_exit(132);
if (ClientSessionInstance().AdoptT0()) ::_exit(133);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0Unavailable, \"no kCapAdoptT0\"}"), 1u) << child.Log;
EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log;
}
// Knob 1 names T1, which P11 closed: one named refusal, the Hello asks T2, never a Fatal.
TEST(RemoteClientAdoptT0, KnobOneIsRefusedByNameAndAsksT2) {
const auto child = RunInChild([] {
MG_Config::Ipc.AdoptTier = 1;
StartControlSession();
if (Srv::ServerSessionInstance().AdoptT0Asked()) ::_exit(140);
MGPHandleOnly handle{};
handle.Kind = static_cast<Uint32>(MGPipeKind::Buffer);
if (MGPipeRouteMapPersistent(handle, 64, nullptr) != nullptr) ::_exit(141);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptTierClosed, \"T1\"}"), 1u) << child.Log;
EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log;
}
// The host's own platform: the grant refuses by name (no AHB) and the client falls back by name.
TEST(RemoteClientAdoptT0, NoAhardwareBufferOnThisPlatformIsT2AndTwoNamedLines) {
const auto child = RunInChild([] {
MG_Config::Ipc.AdoptTier = 0;
MG_Pipe::MGPipeSetResourceOps(&g_fakeOps); // a backend WITH the members: the platform decides
StartControlSession();
GrantAtFirstBind();
if (Srv::ServerSessionInstance().AdoptT0()) ::_exit(150);
if (ClientSessionInstance().AdoptT0()) ::_exit(151);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0Unavailable, \"no AHardwareBuffer\"}"), 1u) << child.Log;
EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0Unavailable, \"no kCapAdoptT0\"}"), 1u) << child.Log;
}
// The server's allow switch off: T2 and a named line, even where T0 would otherwise work.
TEST(RemoteClientAdoptT0, TheServersAllowSwitchOffIsT2AndANamedLine) {
const auto child = RunInChild([] {
MG_Config::Ipc.AllowAdoptT0 = 0;
std::vector<Uint8> seed;
if (AdoptOneStore(0x11, seed) != nullptr) ::_exit(160);
if (g_fakeImports.count != 0) ::_exit(161);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0Unavailable, \"disallowed\"}"), 1u) << child.Log;
EXPECT_NE(child.Log.find("MOBILEGL_IPC_ALLOW_ADOPT_T0=0"), std::string::npos) << child.Log;
}
// THE WHOLE PATH: granted, the caps bit read, the store allocated and seeded from the shadow,
// its Offer queued before its record, matched by seq, imported - and the importer holds exactly
// the client's bytes, the client's pointer is the store, and both arms count it.
// RED-ONCE (host): do not publish kCapAdoptT0 (ServerSession::PublishCapsSnapshot) -> the client
// never adopts, 170.
TEST(RemoteClientAdoptT0, AGrantedStoreLandsWithTheClientsBytes) {
const auto child = RunInChild([] {
std::vector<Uint8> seed;
void* adopted = AdoptOneStore(0x22, seed);
if (adopted == nullptr) ::_exit(170);
if (std::memcmp(adopted, seed.data(), kT0Bytes) != 0) ::_exit(171);
if (g_fakeImports.count != 1 || g_fakeImports.size != kT0Bytes) ::_exit(172);
if (g_fakeImports.handle.Slot != 5 || g_fakeImports.handle.Gen != 1) ::_exit(173);
if (g_fakeImports.bytes != seed) ::_exit(174);
// The import and the client's pointer are one store: a write through either is the other's.
static_cast<Uint8*>(adopted)[100] = 0xA5;
if (static_cast<const Uint8*>(static_cast<FakeAhb*>(g_fakeImports.held)->map)[100] != 0xA5) ::_exit(175);
if (ClientSessionInstance().T0StoresImported() != 1 || ClientSessionInstance().T0BytesImported() != kT0Bytes)
::_exit(176);
if (Srv::ServerSessionInstance().T0StoresImported() != 1) ::_exit(177);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
EXPECT_NE(child.Log.find("T0 granted"), std::string::npos) << child.Log;
EXPECT_NE(child.Log.find("T0 imported store {slot=5, gen=1} - 65536 bytes"), std::string::npos) << child.Log;
EXPECT_NE(child.Log.find("T0 store {slot=5, gen=1} adopted - 65536 bytes"), std::string::npos) << child.Log;
EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log;
}
// ORDER: the Offer arrives 300 ms AFTER its record was published and decoded - the apply thread
// waits for it (bounded) and the store still lands with the client's bytes.
TEST(RemoteClientAdoptT0, AnOfferThatArrivesAfterItsRecordStillLands) {
const auto child = RunInChild([] {
ClientSession::SetT0OfferFaultForTest(300);
std::vector<Uint8> seed;
const auto start = std::chrono::steady_clock::now();
void* adopted = AdoptOneStore(0x33, seed);
const auto waited = std::chrono::steady_clock::now() - start;
if (adopted == nullptr) ::_exit(180);
if (g_fakeImports.bytes != seed) ::_exit(181);
if (waited < std::chrono::milliseconds(300)) ::_exit(182); // it really waited for the late Offer
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
EXPECT_EQ(child.Log.find("AdoptT0NoStore"), std::string::npos) << child.Log;
}
// A LOST Offer is a NAMED DECLINE within the bounded wait - never a hang: the store runs T2.
TEST(RemoteClientAdoptT0, AnOfferThatNeverArrivesIsANamedDeclineNotAHang) {
const auto child = RunInChild([] {
ClientSession::SetT0OfferFaultForTest(-1);
std::vector<Uint8> seed;
const auto start = std::chrono::steady_clock::now();
void* adopted = AdoptOneStore(0x44, seed);
const auto waited = std::chrono::steady_clock::now() - start;
if (adopted != nullptr) ::_exit(190);
if (g_fakeImports.count != 0) ::_exit(191);
if (waited > std::chrono::milliseconds(T0::kOfferWaitMs + 3000)) ::_exit(192);
if (ClientSessionInstance().T0Declined() != 1) ::_exit(193);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
EXPECT_EQ(CountOf(child.Log, "Refuse{AdoptT0NoStore, \"record"), 1u) << child.Log;
EXPECT_EQ(child.Log.find("Fatal{"), std::string::npos) << child.Log;
}
#endif
+3 -48
View File
@@ -324,54 +324,9 @@ TEST(RemoteClientControls, MapPersistentErrorIsNotADecline) {
ExpectNamedAbort(child, "Fatal{ReplyError, \"map_persistent\"}");
}
// P11 A1 (CONTRACT-P11 §1): MOBILEGL_IPC_ADOPT_TIER=0/1 OVER SHARED SEGMENTS DIES AT THE
// HANDSHAKE, before any record. inproc is shared segments by construction, and this is the real
// Start (the same StartOverTransportPair the lanes run). The child writes 'S' once Start returns
// and 'M' once its map_persistent went out, so "died before the first map_persistent" is a fact
// the parent reads rather than infers from the log. The stream half is AdoptTierStreamTest.
//
// RED-ONCE: move the check back to use (drop the client's SessionFail in
// Transport::SettleAdoptTierAtHandshake and make Client::AdoptTierIsEmulate abort on 0/1 again)
// -> the session comes up ('S'), the map_persistent goes out, and the child dies only when the
// server's codec asks the tier.
namespace {
void ExpectDeathAtTheSharedSegmentHandshake(Uint32 tier) {
int marks[2] = {-1, -1};
ASSERT_EQ(::pipe(marks), 0);
const auto child = RunInChild([&marks, tier] {
::close(marks[0]);
MG_Config::Ipc.AdoptTier = tier;
StartControlSession();
const char started = 'S';
(void)!::write(marks[1], &started, 1);
MGPHandleOnly handle{};
handle.Kind = static_cast<Uint32>(MGPipeKind::Buffer);
(void)MGPipeRouteMapPersistent(handle, 64, nullptr);
const char sent = 'M';
(void)!::write(marks[1], &sent, 1);
});
::close(marks[1]);
std::string seen;
char c = 0;
while (::read(marks[0], &c, 1) == 1) seen.push_back(c);
::close(marks[0]);
const std::string word = "Fatal{UnimplementedAdoptTier, \"T" + std::to_string(tier) + "\"}";
ExpectNamedAbort(child, word.c_str());
EXPECT_EQ(seen, "") << "'S' = the session came up, 'M' = a map_persistent went out, both before "
"the death; T" << tier << " over shared segments must die at the handshake\n"
<< child.Log;
EXPECT_NE(child.Log.find("refused at the handshake over shared segments"), std::string::npos)
<< child.Log;
}
} // namespace
TEST(RemoteClientControls, AdoptTierZeroOverSharedSegmentsDiesAtTheHandshakeBeforeAnyMapPersistent) {
ExpectDeathAtTheSharedSegmentHandshake(0u);
}
TEST(RemoteClientControls, AdoptTierOneOverSharedSegmentsDiesAtTheHandshakeBeforeAnyMapPersistent) {
ExpectDeathAtTheSharedSegmentHandshake(1u);
}
// P11 A1's two handshake-Fatal cases (AdoptTier{Zero,One}OverSharedSegmentsDiesAtTheHandshake...)
// are rewritten for B2's fallback semantics in RemoteClientAdoptT0.inc (ID-P11-14: a T0 this
// session cannot use is T2 plus one named line, never a Fatal).
TEST(RemoteClientControls, AnIdenticalRepeatedMakeCurrentRepublishesNothing) {
const auto child = RunInChild([] {
@@ -1721,6 +1721,7 @@ TEST(RemoteReadback, AReplyIsScatteredOnlyWhenItIsOkAndExactlyTheReadsExtent) {
}
#include "RemoteClientControls.inc"
#include "RemoteClientAdoptT0.inc"
#include <MG_Impl/Pipe/FramebufferEmit.h>
#include <MG_Impl/Pipe/TextureEmit.h>
#include <MG_State/GLState/TextureState/TextureObject2D.h>
@@ -261,6 +261,66 @@ TEST(SessionHandshakeTest, WireLayoutMismatchReturnsNamedRefuseWithoutAborting)
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint() ^ 1, BuildFingerprint(),
::MobileGL::Wire::DialMode::No, ::MobileGL::Wire::RefuseCode::WireFingerprint);
}
// P11 B2 (CONTRACT-P11 B2): CONTROL REVISION 5. A peer built at revision 4 - before
// LinkTerms.adoptTier - carries the fingerprint its revision mixed, and is refused by name
// (Refuse{WireFingerprint}), never read as a revision-5 Hello that happens to ask T2. The same shape
// PAIR's 3 -> 4 had. RED ONCE by leaving MOBILEGL_PROTOCOL_CONTROL_REVISION at 4 with the schema
// change: the two fingerprints are then equal and the Welcome goes out.
TEST(SessionHandshakeTest, ARevisionFourPeerIsRefusedByNameAtRevisionFive) {
static_assert(MOBILEGL_PROTOCOL_CONTROL_REVISION == 5, "B2 bumped the control revision 4 -> 5");
auto inputs = CapsAbiFingerprintInputs();
#if MOBILEGL_BUILD_DISAGGREGATED
inputs.ControlSchemaRevision = (static_cast<Uint64>(4) << 32) | MG_Pipe::kMGPipeResourceRespecifyExtentCarrierRevision;
#else
inputs.ControlSchemaRevision = 4;
#endif
const Uint64 revisionFour = Transport::MixAbiFingerprint(inputs);
ASSERT_NE(revisionFour, WireFingerprint());
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, revisionFour, BuildFingerprint(), ::MobileGL::Wire::DialMode::Connect,
::MobileGL::Wire::RefuseCode::WireFingerprint);
}
// P11 B2: the Hello's adoptTier is an ASK of 2 or (shared segments) 0 - T1 is closed and never sent,
// and anything else is not an ask this revision knows. Refuse{LinkTerms} by name; the echo of a
// legal ask is the Welcome's.
TEST(SessionHandshakeTest, AHelloAskingAnAdoptTierThatIsNotAnAskIsRefusedByName) {
using namespace ::MobileGL::Wire;
for (const Uint8 ask : {Uint8{0}, Uint8{1}, Uint8{2}, Uint8{7}}) {
SCOPED_TRACE(static_cast<int>(ask));
std::unique_ptr<Transport::InProcessTransport> client, server;
Transport::InProcessTransport::CreatePair(client, server);
::flatbuffers::FlatBufferBuilder builder(512);
auto terms = CreateLinkTerms(builder, DataPlane::SharedSegments, WireForm::StructImage, 0, 0, 0, 0, ask);
auto hello = CreateHelloDirect(builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR,
BuildFingerprint(), 0, 1, nullptr, WireFingerprint(), WireFingerprint(),
terms, nullptr, DialMode::No);
auto root = CreateCtrlEnvelope(builder, CtrlMsg::Hello, hello.Union());
FinishCtrlEnvelopeBuffer(builder, root);
std::vector<Uint8> first(builder.GetBufferPointer(), builder.GetBufferPointer() + builder.GetSize());
Server::ServerSession session;
Transport::SessionSegmentSizes sizes;
sizes.CmdRingBytes = 4096; sizes.StageBytes = 4096;
sizes.ReplyBytes = 4096; sizes.EventRingBytes = 4096;
session.SetSegmentSizes(sizes);
const Bool legal = ask == 0 || ask == 2;
EXPECT_EQ(session.Accept(*server, &first), legal ? MOBILEGL_OK : MOBILEGL_ERR_PROTOCOL_MISMATCH);
const auto reply = ReadHandshakeFrame(*client);
ASSERT_FALSE(reply.empty());
const auto* envelope = GetCtrlEnvelope(reply.data());
if (legal) {
ASSERT_NE(envelope->msg_as_Welcome(), nullptr);
ASSERT_NE(envelope->msg_as_Welcome()->linkTerms(), nullptr);
EXPECT_EQ(envelope->msg_as_Welcome()->linkTerms()->adoptTier(), ask) << "the Welcome echoes the ask";
EXPECT_EQ(session.AdoptT0Asked(), ask == 0);
EXPECT_FALSE(session.AdoptT0()) << "no grant before the first native bind";
} else {
ASSERT_NE(envelope->msg_as_Refuse(), nullptr);
EXPECT_EQ(envelope->msg_as_Refuse()->code(), RefuseCode::LinkTerms);
}
session.Close();
}
}
TEST(SessionHandshakeTest, ConnectAcceptsDifferentBuildWithIdenticalWire) {
ScopedHandshakeEnvironment required("MOBILEGL_IPC_REQUIRE_SAME_BUILD", "0");
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint(), "different-commit",
+6 -8
View File
@@ -141,14 +141,12 @@ MAGMA_SERVER_ENV_KNOB_NO_TCP = (".ShaderMip1.", ".ShaderMip2.", ".DepthMip.",
f".SplitRecording64.OversizedRecordingSplitScenario.{case}" for case in SPLIT_RECORDING_CASES)
# P11 A1: THE MIRROR SHAPE - entries that exist on tcp ALONE, by construction. `.AdoptTier0.` runs
# LargeArenaAdoptionScenario with MOBILEGL_IPC_ADOPT_TIER=0 on the CLIENT: a stream data plane
# refuses that tier by name and runs T2 (the scenario is green there), while over shared segments -
# the split and spawn arms - the same knob dies at the handshake by design (CONTRACT-P11 §1), so a
# split or spawn copy could only ever be a death. Each tail must match at least one tcp entry, or
# the exception names nothing. (The DirectGLES `.AdoptTier0.` twins need no entry: the Espryt
# CASE pattern does not parse a middle segment, so they never enter that comparison.)
MAGMA_TCP_ONLY = (".AdoptTier0.",)
# THE MIRROR SHAPE - entries that exist on tcp ALONE, by construction. Each tail must match at least
# one tcp entry, or the exception names nothing. P11 A1 put `.AdoptTier0.` here (over shared segments
# MOBILEGL_IPC_ADOPT_TIER=0 died at the handshake); P11 B2 made that knob an ASK that falls back to T2
# by name, so the Magma `.AdoptTier0.` lane now runs on all three arms with one case set and is
# compared like any other. The mechanism stays for the next tcp-only lane.
MAGMA_TCP_ONLY = ()
def lane_names(build_dir, label):