mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user