mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
[Test] (Pipe): pin the P1 poison and verify shapes - 63 fields, 69 verbs, the seven sticky fields, an omitted GenerateMipmap field leaves exactly that field stale, a corrupted snapshot names its field, reading an unfilled field aborts with SIGABRT
- PipeCatalogueTest (header-only, every build): VerbTableIsTheFunctionTable (69 verbs, 9 non-empty classes, the seven sticky bits in every class mask, D7's edges), StickyFieldsAreExactlyTheSeven, FloatVectorsCompareBitwise (a NaN FloatVec4 equals itself, -0.0f differs from 0.0f, a differing BlendStates[3] names RenderState then BlendStates), SixValueStructsHaveFieldLists (69 payloads, PixelStoreParameters and MGHostSpan compared member-wise with Pad0 ignored). - PipeInputsTest, a new unit target linking the static library with its own main() that points MOBILEGL_LOG_FILE_PATH at a temp file: a fake GLContext, the real filler and accessors. OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale (G5 layer 1), ReadingAnOmittedFieldAbortsNamingTheVerb (G5 layer 2: fork, the child reads the omitted field after a draw and a sibling read that must not abort, the parent expects SIGABRT and the exact Fatal line and no @DrawArrays), ReadingAFilledFieldCompletes (the sibling without the omission: _exit(0), no Fatal), CorruptedSnapshotFieldIsNamedWithItsSerial (G4 at block level: clean compare true, a corrupted GetRenderStateParameters is named, its stamp is the fill serial, a corruption outside the mask is not seen), EveryVerbFillsItsClassAndNothingElse (after each of the 69 fills a field is fresh iff its class bit is set). - Every PipeInputsTest case is a visible GTEST_SKIP in a pull build and the poison/verify cases skip in a push build without them; the ctest name set is the same in all three configurations (additions only: 9 names).
This commit is contained in:
@@ -24,5 +24,35 @@ if (MSVC)
|
|||||||
target_compile_options(PipeCatalogueTest PRIVATE /Zc:preprocessor)
|
target_compile_options(PipeCatalogueTest PRIVATE /Zc:preprocessor)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The P1 poison and verify shapes at the block level: a fake GLContext, the real filler and
|
||||||
|
# accessors out of the static library. Links gtest (not gtest_main): the suite needs its own
|
||||||
|
# main() to point MOBILEGL_LOG_FILE_PATH at a temp file before anything logs, because its
|
||||||
|
# abort cases read the Fatal line back out of that file. Every case is a visible SKIP in a
|
||||||
|
# pull build.
|
||||||
|
add_executable(
|
||||||
|
PipeInputsTest
|
||||||
|
PipeInputsTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(PipeInputsTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
${MGL_ROOT}/MobileGL/MG_Pipe
|
||||||
|
${MGL_ROOT}/3rdparty/xxHash
|
||||||
|
${MGL_ROOT}/3rdparty/Vulkan-Headers/include
|
||||||
|
${MGL_ROOT}/3rdparty/SPIRV-Reflect
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
PipeInputsTest PRIVATE
|
||||||
|
GTest::gtest
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
target_compile_options(PipeInputsTest PRIVATE /Zc:preprocessor)
|
||||||
|
endif()
|
||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
gtest_discover_tests(PipeCatalogueTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
gtest_discover_tests(PipeInputsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
#include "Includes.h"
|
#include "Includes.h"
|
||||||
#include <MG_Pipe/MGPipe.h>
|
#include <MG_Pipe/MGPipe.h>
|
||||||
@@ -233,6 +234,116 @@ TEST(PipeCatalogue, PipeInputFieldsStartUnfilled) {
|
|||||||
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
|
EXPECT_FALSE(MGPipeInputFieldIsFresh(state, MGPipeInputField::GetRenderStateParameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// G5b: the verb enum is GLFunctionsTable's member list (69 entries), every class has verbs,
|
||||||
|
// and the seven sticky fields ride in every class mask (P1 brief D7).
|
||||||
|
TEST(PipeCatalogue, VerbTableIsTheFunctionTable) {
|
||||||
|
EXPECT_EQ(kMGPipeVerbCount, 69u);
|
||||||
|
EXPECT_EQ(kMGPipeVerbClassCount, 9u);
|
||||||
|
SizeT perClass[kMGPipeVerbClassCount] = {};
|
||||||
|
for (SizeT v = 0; v < kMGPipeVerbCount; ++v) {
|
||||||
|
++perClass[static_cast<SizeT>(kMGPipeVerbClass[v])];
|
||||||
|
}
|
||||||
|
for (SizeT c = 0; c < kMGPipeVerbClassCount; ++c) {
|
||||||
|
EXPECT_GT(perClass[c], 0u) << kMGPipeVerbClassNames[c];
|
||||||
|
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
||||||
|
if (kMGPipeInputFieldSticky[f]) {
|
||||||
|
EXPECT_TRUE(MGPipeFieldMaskHas(kMGPipeClassFieldMask[c], static_cast<MGPipeInputField>(f)))
|
||||||
|
<< kMGPipeInputFieldNames[f] << " in " << kMGPipeVerbClassNames[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The class table of D7, spot-checked at its edges: a draw reads the render state, a
|
||||||
|
// query reads only the paused-primitive counter, and GenerateMipmap is a texture op.
|
||||||
|
const auto& draw = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kDraw)];
|
||||||
|
const auto& query = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kQuery)];
|
||||||
|
EXPECT_TRUE(MGPipeFieldMaskHas(draw, MGPipeInputField::GetRenderStateParameters));
|
||||||
|
EXPECT_FALSE(MGPipeFieldMaskHas(query, MGPipeInputField::GetRenderStateParameters));
|
||||||
|
EXPECT_TRUE(MGPipeFieldMaskHas(query, MGPipeInputField::GetTransformFeedbackPausedPrimitiveCounter));
|
||||||
|
EXPECT_EQ(kMGPipeVerbClass[static_cast<SizeT>(MGPipeVerb::GenerateMipmap)], MGPipeVerbClass::kTextureOp);
|
||||||
|
EXPECT_STREQ(kMGPipeVerbNames[static_cast<SizeT>(MGPipeVerb::GetGpuTimestampNs)], "GetGpuTimestampNs");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sticky set is exactly the seven forwarded, argument-keyed accessors (P1 brief D6); no
|
||||||
|
// version or generation accessor is among them.
|
||||||
|
TEST(PipeCatalogue, StickyFieldsAreExactlyTheSeven) {
|
||||||
|
const char* const expected[] = {"GetBufferBindingPointCount", "GetProgramObject", "GetTextureObject",
|
||||||
|
"HasOpenTransformFeedbackSpan", "InvalidateCompileEnv", "ValidateProgramName",
|
||||||
|
"RecordError"};
|
||||||
|
SizeT count = 0;
|
||||||
|
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
||||||
|
Bool listed = false;
|
||||||
|
for (const char* name : expected) {
|
||||||
|
if (std::strcmp(kMGPipeInputFieldNames[f], name) == 0) listed = true;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(kMGPipeInputFieldSticky[f], listed) << kMGPipeInputFieldNames[f];
|
||||||
|
if (kMGPipeInputFieldSticky[f]) ++count;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(count, 7u);
|
||||||
|
EXPECT_EQ(kMGPipeInputStickyFieldCount, 7u);
|
||||||
|
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetTextureContextId)]);
|
||||||
|
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetSamplingResolutionGeneration)]);
|
||||||
|
EXPECT_FALSE(kMGPipeInputFieldSticky[static_cast<SizeT>(MGPipeInputField::GetPipelineStateVersion)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// G4 compares floating point BY BITS (P1 brief D8): a NaN equals itself, a negative zero
|
||||||
|
// does not equal a positive one, and a vector type inside an Array inside a value struct is
|
||||||
|
// reached field by field - the differing member of the residual block is named.
|
||||||
|
TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
|
||||||
|
const Float nan = std::numeric_limits<Float>::quiet_NaN();
|
||||||
|
const FloatVec4 a{nan, 1.f, 2.f, 3.f};
|
||||||
|
const FloatVec4 b{nan, 1.f, 2.f, 3.f};
|
||||||
|
EXPECT_TRUE(MGPipeFieldEqual(a, b));
|
||||||
|
EXPECT_FALSE(a == b); // IEEE ==, the comparison the comparator must NOT use
|
||||||
|
const FloatVec4 zero{0.f, 0.f, 0.f, 0.f};
|
||||||
|
const FloatVec4 negativeZero{-0.f, 0.f, 0.f, 0.f};
|
||||||
|
EXPECT_FALSE(MGPipeFieldEqual(zero, negativeZero));
|
||||||
|
EXPECT_TRUE(zero == negativeZero);
|
||||||
|
EXPECT_TRUE(MGPipeFieldEqual(1.5f, 1.5f));
|
||||||
|
EXPECT_FALSE(MGPipeFieldEqual(-0.f, 0.f));
|
||||||
|
|
||||||
|
ResidualValueBlock left{};
|
||||||
|
ResidualValueBlock right{};
|
||||||
|
const char* field = nullptr;
|
||||||
|
EXPECT_TRUE(MGPipeVerify(left, right, &field));
|
||||||
|
right.RenderState.BlendStates[3].SrcFactorRGB = BlendFactor::DstColor;
|
||||||
|
EXPECT_FALSE(MGPipeVerify(left, right, &field));
|
||||||
|
EXPECT_STREQ(field, "RenderState");
|
||||||
|
const char* inner = nullptr;
|
||||||
|
EXPECT_FALSE(MGPipeVerify(left.RenderState, right.RenderState, &inner));
|
||||||
|
EXPECT_STREQ(inner, "BlendStates");
|
||||||
|
// A NaN patch level in the render state equals itself too.
|
||||||
|
right = left;
|
||||||
|
left.RenderState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
|
||||||
|
right.RenderState.PatchDefaultOuterLevel = FloatVec4{nan, 1.f, 1.f, 1.f};
|
||||||
|
EXPECT_TRUE(MGPipeVerify(left, right, &field));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The six value structs have field lists of their own (P1 brief D8): 63 + 6 payloads, and
|
||||||
|
// the struct that used to memcmp is compared member by member.
|
||||||
|
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
||||||
|
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 69u);
|
||||||
|
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
|
||||||
|
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
|
||||||
|
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
||||||
|
static_assert(MGPipeHasFieldVerifier<StencilFaceState>::value);
|
||||||
|
static_assert(MGPipeHasFieldVerifier<DynamicBackendParameters>::value);
|
||||||
|
static_assert(MGPipeHasFieldVerifier<MGHostSpan>::value);
|
||||||
|
PixelStoreParameters p{};
|
||||||
|
PixelStoreParameters q{};
|
||||||
|
const char* field = nullptr;
|
||||||
|
EXPECT_TRUE(MGPipeVerify(p, q, &field));
|
||||||
|
q.SkipRows = 2;
|
||||||
|
EXPECT_FALSE(MGPipeVerify(p, q, &field));
|
||||||
|
EXPECT_STREQ(field, "SkipRows");
|
||||||
|
MGHostSpan s{};
|
||||||
|
MGHostSpan t{};
|
||||||
|
t.Pad0 = 0x5A; // padding is not a field
|
||||||
|
EXPECT_TRUE(MGPipeVerify(s, t, &field));
|
||||||
|
t.Offset = 8;
|
||||||
|
EXPECT_FALSE(MGPipeVerify(s, t, &field));
|
||||||
|
EXPECT_STREQ(field, "Offset");
|
||||||
|
}
|
||||||
|
|
||||||
// G7 pins the member list the pipeline/dynamic split is derived from.
|
// G7 pins the member list the pipeline/dynamic split is derived from.
|
||||||
TEST(PipeCatalogue, PipelineSubsetMembersArePinned) {
|
TEST(PipeCatalogue, PipelineSubsetMembersArePinned) {
|
||||||
EXPECT_EQ(kMGPipePipelineStateMemberCount, 24u);
|
EXPECT_EQ(kMGPipePipelineStateMemberCount, 24u);
|
||||||
|
|||||||
Executable
+270
@@ -0,0 +1,270 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/Pipe/PipeInputsTest.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
|
||||||
|
|
||||||
|
// The P1 poison and verify shapes at the block level (P1 brief C.1): a fake GLContext, the
|
||||||
|
// real filler, the real accessors. Needs the push sources, so every case is a visible SKIP
|
||||||
|
// in a pull build rather than a vanishing test. The abort cases fork (HeadlessGL.cpp's
|
||||||
|
// pre-flight shape): the child performs the read that must be Fatal{UnmigratedPipeInput}
|
||||||
|
// and the parent reads SIGABRT out of waitpid and the exact line out of the log file that
|
||||||
|
// main() below points MOBILEGL_LOG_FILE_PATH at (LogLevelTest's shape). Never EXPECT_DEATH.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include <MG_Pipe/MGPipe.h>
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||||
|
#include <MG_Impl/Pipe/PipeFill.h>
|
||||||
|
#include <MG_State/GLState/Core.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
#include <csignal>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#define MGTEST_HAVE_FORK 1
|
||||||
|
#else
|
||||||
|
#define MGTEST_HAVE_FORK 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Pipe;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::string g_logPath;
|
||||||
|
|
||||||
|
std::string ReadLog() {
|
||||||
|
std::ifstream in(g_logPath, std::ios::binary);
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << in.rdbuf();
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_PUSH
|
||||||
|
using GLContext = MG_State::GLState::GLContext;
|
||||||
|
|
||||||
|
// A live frontend context for the filler to read (SanityTest's idiom), restored on the
|
||||||
|
// way out so the cases stay independent.
|
||||||
|
class PipeInputsTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
m_previous = Move(MG_State::pGLContext);
|
||||||
|
MG_State::pGLContext = MakeUnique<GLContext>();
|
||||||
|
MGPipeSetPoisonOmission(nullptr, nullptr);
|
||||||
|
}
|
||||||
|
void TearDown() override {
|
||||||
|
MGPipeSetPoisonOmission(nullptr, nullptr);
|
||||||
|
MG_State::pGLContext = Move(m_previous);
|
||||||
|
}
|
||||||
|
UniquePtr<GLContext> m_previous;
|
||||||
|
};
|
||||||
|
|
||||||
|
#if MOBILEGL_PIPE_POISON
|
||||||
|
Bool Fresh(MGPipeInputField field) { return MGPipeInputFieldIsFresh(gPipeInputs.FilledState(), field); }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
[[maybe_unused]] constexpr const char* kOmittedFatal ="Fatal{UnmigratedPipeInput, \"GetActiveTextureUnit@GenerateMipmap\"}";
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#if !MOBILEGL_PIPE_PUSH
|
||||||
|
|
||||||
|
TEST(PipeInputsTest,OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
|
||||||
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||||
|
}
|
||||||
|
TEST(PipeInputsTest,ReadingAnOmittedFieldAbortsNamingTheVerb) {
|
||||||
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||||
|
}
|
||||||
|
TEST(PipeInputsTest,ReadingAFilledFieldCompletes) {
|
||||||
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||||
|
}
|
||||||
|
TEST(PipeInputsTest,CorruptedSnapshotFieldIsNamedWithItsSerial) {
|
||||||
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||||
|
}
|
||||||
|
TEST(PipeInputsTest,EveryVerbFillsItsClassAndNothingElse) {
|
||||||
|
GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)";
|
||||||
|
}
|
||||||
|
|
||||||
|
#else // MOBILEGL_PIPE_PUSH
|
||||||
|
|
||||||
|
// Negative control B, layer 1 (P1 brief D6 / G5): the omitted (verb, field) pair is the
|
||||||
|
// ONLY thing that goes stale - a sibling field of the same verb is fresh, and the verb after
|
||||||
|
// it neither heals the field (not in kDraw's mask) nor loses one of its own.
|
||||||
|
TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) {
|
||||||
|
#if !MOBILEGL_PIPE_POISON
|
||||||
|
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
|
||||||
|
#else
|
||||||
|
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||||
|
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||||
|
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
|
||||||
|
|
||||||
|
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
|
||||||
|
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||||
|
EXPECT_TRUE(Fresh(MGPipeInputField::GetTextureUnitObject));
|
||||||
|
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||||
|
// The value was still copied: only the stamp is withheld.
|
||||||
|
EXPECT_EQ(gPipeInputs.CurrentVerb(), MGPipeVerb::GenerateMipmap);
|
||||||
|
|
||||||
|
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||||
|
EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||||
|
EXPECT_TRUE(Fresh(MGPipeInputField::GetBoundVertexArray));
|
||||||
|
EXPECT_TRUE(Fresh(MGPipeInputField::GetRenderStateParameters));
|
||||||
|
// A sticky field stays fresh across every verb.
|
||||||
|
EXPECT_TRUE(Fresh(MGPipeInputField::RecordError));
|
||||||
|
|
||||||
|
// And the omission is scoped to its verb: a different verb of the same class keeps it.
|
||||||
|
MGPipeFillForVerb(MGPipeVerb::BindImageTexture);
|
||||||
|
EXPECT_TRUE(Fresh(MGPipeInputField::GetActiveTextureUnit));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative control B, layer 2 (G5): the read itself. The child fills GenerateMipmap with the
|
||||||
|
// omission and reads gPipeInputs.GetActiveTextureUnit(); the parent expects SIGABRT and the
|
||||||
|
// exact Fatal line, and that nothing else was fatal.
|
||||||
|
TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) {
|
||||||
|
#if !MOBILEGL_PIPE_POISON
|
||||||
|
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
|
||||||
|
#elif !MGTEST_HAVE_FORK
|
||||||
|
GTEST_SKIP() << "no fork() on this platform";
|
||||||
|
#else
|
||||||
|
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||||
|
const std::string before = ReadLog();
|
||||||
|
std::fflush(nullptr);
|
||||||
|
const pid_t pid = ::fork();
|
||||||
|
ASSERT_GE(pid, 0);
|
||||||
|
if (pid == 0) {
|
||||||
|
// Child: no gtest assertions, _exit never exit.
|
||||||
|
MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit");
|
||||||
|
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||||
|
(void)gPipeInputs.GetRenderStateParameters(); // a filled field of the preceding draw: must not abort
|
||||||
|
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||||
|
(void)gPipeInputs.GetTextureUnitObject(0); // the sibling field: filled, must not abort
|
||||||
|
(void)gPipeInputs.GetActiveTextureUnit(); // the omitted field: Fatal
|
||||||
|
::_exit(3); // reached only if the poison failed
|
||||||
|
}
|
||||||
|
int status = 0;
|
||||||
|
ASSERT_EQ(::waitpid(pid, &status, 0), pid);
|
||||||
|
ASSERT_TRUE(WIFSIGNALED(status)) << "child exited normally with " << (WIFEXITED(status) ? WEXITSTATUS(status) : -1);
|
||||||
|
EXPECT_EQ(WTERMSIG(status), SIGABRT);
|
||||||
|
const std::string log = ReadLog().substr(before.size());
|
||||||
|
EXPECT_NE(log.find(kOmittedFatal), std::string::npos) << log;
|
||||||
|
EXPECT_EQ(log.find("@DrawArrays"), std::string::npos) << log;
|
||||||
|
EXPECT_EQ(log.find("GetTextureUnitObject@"), std::string::npos) << log;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sibling: the same reads without the omission complete, and no Fatal is logged.
|
||||||
|
TEST_F(PipeInputsTest, ReadingAFilledFieldCompletes) {
|
||||||
|
#if !MGTEST_HAVE_FORK
|
||||||
|
GTEST_SKIP() << "no fork() on this platform";
|
||||||
|
#else
|
||||||
|
ASSERT_FALSE(g_logPath.empty()) << "main() did not set MOBILEGL_LOG_FILE_PATH";
|
||||||
|
const std::string before = ReadLog();
|
||||||
|
std::fflush(nullptr);
|
||||||
|
const pid_t pid = ::fork();
|
||||||
|
ASSERT_GE(pid, 0);
|
||||||
|
if (pid == 0) {
|
||||||
|
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||||
|
(void)gPipeInputs.GetRenderStateParameters();
|
||||||
|
MGPipeFillForVerb(MGPipeVerb::GenerateMipmap);
|
||||||
|
(void)gPipeInputs.GetTextureUnitObject(0);
|
||||||
|
(void)gPipeInputs.GetActiveTextureUnit();
|
||||||
|
::_exit(0);
|
||||||
|
}
|
||||||
|
int status = 0;
|
||||||
|
ASSERT_EQ(::waitpid(pid, &status, 0), pid);
|
||||||
|
ASSERT_TRUE(WIFEXITED(status)) << "child died on signal " << (WIFSIGNALED(status) ? WTERMSIG(status) : -1);
|
||||||
|
EXPECT_EQ(WEXITSTATUS(status), 0);
|
||||||
|
const std::string log = ReadLog().substr(before.size());
|
||||||
|
EXPECT_EQ(log.find("Fatal{"), std::string::npos) << log;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative control A at the block level (G4): a clean snapshot compares equal to the pushed
|
||||||
|
// block over the verb's mask; corrupting one field of the snapshot makes the compare name
|
||||||
|
// exactly that field, at the serial of the fill it belongs to.
|
||||||
|
TEST_F(PipeInputsTest, CorruptedSnapshotFieldIsNamedWithItsSerial) {
|
||||||
|
#if !MOBILEGL_PIPE_VERIFY
|
||||||
|
GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)";
|
||||||
|
#else
|
||||||
|
const Uint64 serialBefore = gPipeInputs.FilledState().CurrentVerbSerial;
|
||||||
|
MGPipeFillForVerb(MGPipeVerb::DrawArrays);
|
||||||
|
const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial;
|
||||||
|
EXPECT_EQ(serial, serialBefore + 1);
|
||||||
|
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(MGPipeVerbClass::kDraw)];
|
||||||
|
|
||||||
|
static PipeInputs snapshot{};
|
||||||
|
SnapshotFromGLContext(snapshot, mask);
|
||||||
|
MGPipeInputField field = MGPipeInputField::kFieldCount;
|
||||||
|
EXPECT_TRUE(MGPipeVerifyInputs(gPipeInputs, snapshot, mask, &field));
|
||||||
|
EXPECT_EQ(field, MGPipeInputField::kFieldCount);
|
||||||
|
|
||||||
|
ASSERT_TRUE(MGPipeApplyVerifyCorruption(snapshot, MGPipeInputField::GetRenderStateParameters));
|
||||||
|
EXPECT_FALSE(MGPipeVerifyInputs(gPipeInputs, snapshot, mask, &field));
|
||||||
|
EXPECT_EQ(field, MGPipeInputField::GetRenderStateParameters);
|
||||||
|
EXPECT_STREQ(kMGPipeInputFieldNames[static_cast<SizeT>(field)], "GetRenderStateParameters");
|
||||||
|
// The serial the report would print is the fill's, and it stamped that field.
|
||||||
|
EXPECT_EQ(gPipeInputs.FilledState().FilledGen[static_cast<SizeT>(field)], serial);
|
||||||
|
|
||||||
|
// A forwarded field has nothing to corrupt, and the corruption of a field outside the
|
||||||
|
// mask is not seen by a compare over that mask.
|
||||||
|
EXPECT_FALSE(MGPipeApplyVerifyCorruption(snapshot, MGPipeInputField::RecordError));
|
||||||
|
SnapshotFromGLContext(snapshot, mask);
|
||||||
|
ASSERT_TRUE(MGPipeApplyVerifyCorruption(snapshot, MGPipeInputField::GetPixelStoreParameters));
|
||||||
|
EXPECT_FALSE(MGPipeFieldMaskHas(mask, MGPipeInputField::GetPixelStoreParameters));
|
||||||
|
EXPECT_TRUE(MGPipeVerifyInputs(gPipeInputs, snapshot, mask, &field));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every verb fills exactly its class mask: after a fill, a field is fresh iff its bit is set
|
||||||
|
// (sticky fields included, since every class mask carries them).
|
||||||
|
TEST_F(PipeInputsTest, EveryVerbFillsItsClassAndNothingElse) {
|
||||||
|
#if !MOBILEGL_PIPE_POISON
|
||||||
|
GTEST_SKIP() << "poison not compiled in (MOBILEGL_PIPE_POISON=0)";
|
||||||
|
#else
|
||||||
|
for (SizeT v = 0; v < kMGPipeVerbCount; ++v) {
|
||||||
|
const auto verb = static_cast<MGPipeVerb>(v);
|
||||||
|
MGPipeFillForVerb(verb);
|
||||||
|
const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast<SizeT>(kMGPipeVerbClass[v])];
|
||||||
|
for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) {
|
||||||
|
const auto field = static_cast<MGPipeInputField>(f);
|
||||||
|
EXPECT_EQ(Fresh(field), MGPipeFieldMaskHas(mask, field))
|
||||||
|
<< kMGPipeInputFieldNames[f] << " after " << kMGPipeVerbNames[v];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_EQ(gPipeInputs.ContextIdentity(), static_cast<const void*>(MG_State::pGLContext.get()));
|
||||||
|
EXPECT_TRUE(gPipeInputs.IsLive());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // MOBILEGL_PIPE_PUSH
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
||||||
|
// write, and caches the FILE*.
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
const fs::path path = fs::temp_directory_path() / "mobilegl-pipeinputs-test.log";
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove(path, ec);
|
||||||
|
g_logPath = path.string();
|
||||||
|
#if defined(_WIN32)
|
||||||
|
_putenv_s("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str());
|
||||||
|
#else
|
||||||
|
setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1);
|
||||||
|
#endif
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user