diff --git a/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp b/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp old mode 100755 new mode 100644 index 2d6266eb..8b307535 --- a/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeInputsTest.cpp @@ -12,6 +12,10 @@ // 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. +// +// The log file is per PROCESS: gtest_discover_tests runs every case as its own process, in +// parallel under ctest -j, and a name shared between them let a sibling's unlink or Fatal +// line land in this process's read. A forked child inherits its parent's path on purpose. #include @@ -26,6 +30,7 @@ #include #if MOBILEGL_PIPE_PUSH +#include #include #include #include @@ -37,6 +42,7 @@ #include #define MGTEST_HAVE_FORK 1 #else +#include #define MGTEST_HAVE_FORK 0 #endif @@ -53,6 +59,14 @@ namespace { return ss.str(); } + long ProcessId() { +#if defined(_WIN32) + return static_cast(::_getpid()); +#else + return static_cast(::getpid()); +#endif + } + #if MOBILEGL_PIPE_PUSH using GLContext = MG_State::GLState::GLContext; @@ -77,6 +91,44 @@ namespace { #endif [[maybe_unused]] constexpr const char* kOmittedFatal ="Fatal{UnmigratedPipeInput, \"GetActiveTextureUnit@GenerateMipmap\"}"; + [[maybe_unused]] constexpr const char* kOmissionKnob = "GenerateMipmap:GetActiveTextureUnit"; + +#if MGTEST_HAVE_FORK + struct ChildResult { + int Status = -1; // waitpid's status; -1 when fork or waitpid failed + std::string Log; // the lines the child appended to the log file + }; + + // Runs `body` in a forked child and returns its wait status and log delta. The child + // must not use gtest assertions; it _exit(0)s when `body` returns, so a body that is + // expected to die must be asserted dead by the parent (WIFSIGNALED), never assumed. + template + ChildResult RunInChild(Body body) { + ChildResult result; + const std::string before = ReadLog(); + std::fflush(nullptr); + const pid_t pid = ::fork(); + if (pid < 0) return result; + if (pid == 0) { + body(); + ::_exit(0); + } + int status = 0; + if (::waitpid(pid, &status, 0) != pid) return result; + result.Status = status; + result.Log = ReadLog().substr(before.size()); + return result; + } + + Bool DiedOfAbort(const ChildResult& r) { return WIFSIGNALED(r.Status) && WTERMSIG(r.Status) == SIGABRT; } + Bool ExitedWith(const ChildResult& r, int code) { return WIFEXITED(r.Status) && WEXITSTATUS(r.Status) == code; } + std::string DescribeStatus(const ChildResult& r) { + if (r.Status < 0) return "fork/waitpid failed"; + if (WIFEXITED(r.Status)) return "exited " + std::to_string(WEXITSTATUS(r.Status)); + if (WIFSIGNALED(r.Status)) return "signal " + std::to_string(WTERMSIG(r.Status)); + return "status " + std::to_string(r.Status); + } +#endif // MGTEST_HAVE_FORK #endif // MOBILEGL_PIPE_PUSH } // namespace @@ -97,12 +149,31 @@ TEST(PipeInputsTest,CorruptedSnapshotFieldIsNamedWithItsSerial) { TEST(PipeInputsTest,EveryVerbFillsItsClassAndNothingElse) { GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; } +TEST(PipeInputsTest,MutatedFieldIsNamedAtRead) { + GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; +} +TEST(PipeInputsTest,PoisonOmitKnobArmsTheOmission) { + GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; +} +TEST(PipeInputsTest,BadPoisonOmitKnobIsFatalNamingTheKnob) { + GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; +} +TEST(PipeInputsTest,VerifyCorruptKnobNamesTheFieldAtEntry) { + GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; +} +TEST(PipeInputsTest,BadVerifyCorruptKnobIsFatalNamingTheKnob) { + GTEST_SKIP() << "push not compiled in (MOBILEGL_PIPE_PUSH=OFF)"; +} +TEST(PipeInputsTest,VerifyFatalOffLogsTheDivergenceAndContinues) { + 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. +// ONLY thing that goes stale - every other bit of the verb class's mask is fresh, every +// field outside it is not, 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)"; @@ -117,6 +188,17 @@ TEST_F(PipeInputsTest, OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale) { EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit)); // The value was still copied: only the stamp is withheld. EXPECT_EQ(gPipeInputs.CurrentVerb(), MGPipeVerb::GenerateMipmap); + // Exactly that field: a field is fresh iff its bit is in kTextureOp's mask and it is not + // the omitted one. + { + const auto cls = kMGPipeVerbClass[static_cast(MGPipeVerb::GenerateMipmap)]; + const MGPipeFieldMask& mask = kMGPipeClassFieldMask[static_cast(cls)]; + for (SizeT f = 0; f < kMGPipeInputFieldCount; ++f) { + const auto field = static_cast(f); + const Bool expected = MGPipeFieldMaskHas(mask, field) && field != MGPipeInputField::GetActiveTextureUnit; + EXPECT_EQ(Fresh(field), expected) << kMGPipeInputFieldNames[f] << " after the omitted GenerateMipmap fill"; + } + } MGPipeFillForVerb(MGPipeVerb::DrawArrays); EXPECT_FALSE(Fresh(MGPipeInputField::GetActiveTextureUnit)); @@ -141,12 +223,7 @@ TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) { 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. + const ChildResult r = RunInChild([] { MGPipeSetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit"); MGPipeFillForVerb(MGPipeVerb::DrawArrays); (void)gPipeInputs.GetRenderStateParameters(); // a filled field of the preceding draw: must not abort @@ -154,15 +231,11 @@ TEST_F(PipeInputsTest, ReadingAnOmittedFieldAbortsNamingTheVerb) { (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; + }); + ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_NE(r.Log.find(kOmittedFatal), std::string::npos) << r.Log; + EXPECT_EQ(r.Log.find("@DrawArrays"), std::string::npos) << r.Log; + EXPECT_EQ(r.Log.find("GetTextureUnitObject@"), std::string::npos) << r.Log; #endif } @@ -172,24 +245,62 @@ TEST_F(PipeInputsTest, ReadingAFilledFieldCompletes) { 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) { + const ChildResult r = RunInChild([] { 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; + }); + ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_EQ(r.Log.find("Fatal{"), std::string::npos) << r.Log; +#endif +} + +// The MOBILEGL_PIPE_POISON_OMIT parser, through the Features field the loader fills: the +// child sets the knob after its parent filled without it, and the first fill after that +// parses it, logs the arming line and withholds the stamp exactly as the programmatic form. +TEST_F(PipeInputsTest, PoisonOmitKnobArmsTheOmission) { +#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"; + MGPipeFillForVerb(MGPipeVerb::DrawArrays); // the parent's parse saw an empty knob + const ChildResult r = RunInChild([] { + MG_Config::Features.PipePoisonOmit = kOmissionKnob; + MGPipeFillForVerb(MGPipeVerb::GenerateMipmap); + (void)gPipeInputs.GetTextureUnitObject(0); // the sibling field: filled, must not abort + (void)gPipeInputs.GetActiveTextureUnit(); // the omitted field: Fatal + ::_exit(3); + }); + ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_NE(r.Log.find("MGPipe: poison omission armed - GetActiveTextureUnit@GenerateMipmap"), std::string::npos) + << r.Log; + EXPECT_NE(r.Log.find(kOmittedFatal), std::string::npos) << r.Log; + EXPECT_EQ(r.Log.find("GetTextureUnitObject@"), std::string::npos) << r.Log; +#endif +} + +// An unknown verb in the knob is Fatal{PipeVerifyBadKnob} naming the knob and the value, +// before any stamp is withheld. +TEST_F(PipeInputsTest, BadPoisonOmitKnobIsFatalNamingTheKnob) { +#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 ChildResult r = RunInChild([] { + MG_Config::Features.PipePoisonOmit = "NoSuchVerb:GetActiveTextureUnit"; + MGPipeFillForVerb(MGPipeVerb::GenerateMipmap); + ::_exit(3); + }); + ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_NE(r.Log.find("Fatal{PipeVerifyBadKnob, \"MOBILEGL_PIPE_POISON_OMIT=NoSuchVerb:GetActiveTextureUnit\": " + "no such verb in kMGPipeVerbNames}"), + std::string::npos) + << r.Log; + EXPECT_EQ(r.Log.find("poison omission armed"), std::string::npos) << r.Log; #endif } @@ -229,6 +340,122 @@ TEST_F(PipeInputsTest, CorruptedSnapshotFieldIsNamedWithItsSerial) { #endif } +// The compare-at-read arm (P1 brief D8, the arm that is real in P1): a value that changes in +// the live context between the verb boundary and the read is Fatal{PipeVerifyDiffer, +// "Field@Verb", verb=, where=read}; the same read before the mutation completes. +TEST_F(PipeInputsTest, MutatedFieldIsNamedAtRead) { +#if !MOBILEGL_PIPE_VERIFY + GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)"; +#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"; + MGPipeFillForVerb(MGPipeVerb::Clear); // the parent armed nothing: Features.PipeVerify is false here + const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1; // the child's DrawArrays fill + const ChildResult r = RunInChild([] { + MG_Config::Features.PipeVerify = true; + MGPipeFillForVerb(MGPipeVerb::DrawArrays); + const Float boundary = gPipeInputs.GetLineWidth(); // boundary == live: completes + (void)gPipeInputs.GetRenderStateParameters(); + MG_State::pGLContext->SetLineWidth(boundary + 1.0f); + if (MG_State::pGLContext->GetLineWidth() == boundary) ::_exit(7); // the mutation did not take + (void)gPipeInputs.GetLineWidth(); // the stored value is stale against the live one: Fatal + ::_exit(3); + }); + ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_NE(r.Log.find("MGPipe: verify armed - 63 fields, 69 verbs, fatal=1"), std::string::npos) << r.Log; + EXPECT_NE(r.Log.find("MGPipe: verify read of GetLineWidth (index 0, 0) differs from the live context"), + std::string::npos) + << r.Log; + const std::string expected = "Fatal{PipeVerifyDiffer, \"GetLineWidth@DrawArrays\", verb=" + std::to_string(serial) + + ", where=read}"; + EXPECT_NE(r.Log.find(expected), std::string::npos) << "expected " << expected << " in\n" << r.Log; + EXPECT_EQ(r.Log.find("where=entry"), std::string::npos) << r.Log; +#endif +} + +// The MOBILEGL_PIPE_VERIFY_CORRUPT parser through Features: the fill after the child arms it +// logs both arming lines and the entry compare names the corrupted field at that fill's +// serial, where=entry. +TEST_F(PipeInputsTest, VerifyCorruptKnobNamesTheFieldAtEntry) { +#if !MOBILEGL_PIPE_VERIFY + GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)"; +#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"; + MGPipeFillForVerb(MGPipeVerb::Clear); + const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1; + const ChildResult r = RunInChild([] { + MG_Config::Features.PipeVerify = true; + MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters"; + MGPipeFillForVerb(MGPipeVerb::DrawArrays); + ::_exit(3); + }); + ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_NE(r.Log.find("MGPipe: verify armed - 63 fields, 69 verbs, fatal=1"), std::string::npos) << r.Log; + EXPECT_NE(r.Log.find("MGPipe: verify corruption armed - GetRenderStateParameters"), std::string::npos) << r.Log; + const std::string expected = "Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@DrawArrays\", verb=" + + std::to_string(serial) + ", where=entry}"; + EXPECT_NE(r.Log.find(expected), std::string::npos) << "expected " << expected << " in\n" << r.Log; +#endif +} + +// An unknown field name in MOBILEGL_PIPE_VERIFY_CORRUPT is Fatal{PipeVerifyBadKnob} at +// arming, before the comparator reports anything. +TEST_F(PipeInputsTest, BadVerifyCorruptKnobIsFatalNamingTheKnob) { +#if !MOBILEGL_PIPE_VERIFY + GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)"; +#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 ChildResult r = RunInChild([] { + MG_Config::Features.PipeVerify = true; + MG_Config::Features.PipeVerifyCorrupt = "NoSuchField"; + MGPipeFillForVerb(MGPipeVerb::DrawArrays); + ::_exit(3); + }); + ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_NE(r.Log.find("Fatal{PipeVerifyBadKnob, \"MOBILEGL_PIPE_VERIFY_CORRUPT=NoSuchField\": " + "no such field in kMGPipeInputFieldNames}"), + std::string::npos) + << r.Log; + EXPECT_EQ(r.Log.find("verify armed"), std::string::npos) << r.Log; + EXPECT_EQ(r.Log.find("PipeVerifyDiffer"), std::string::npos) << r.Log; +#endif +} + +// MOBILEGL_PIPE_VERIFY_FATAL=0: the divergence is logged with its field and serial and the +// process goes on (the fill returns, the next fill's compare runs again). +TEST_F(PipeInputsTest, VerifyFatalOffLogsTheDivergenceAndContinues) { +#if !MOBILEGL_PIPE_VERIFY + GTEST_SKIP() << "verify not compiled in (MOBILEGL_PIPE_VERIFY=OFF)"; +#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"; + MGPipeFillForVerb(MGPipeVerb::Clear); + const Uint64 serial = gPipeInputs.FilledState().CurrentVerbSerial + 1; + const ChildResult r = RunInChild([] { + MG_Config::Features.PipeVerify = true; + MG_Config::Features.PipeVerifyFatal = false; + MG_Config::Features.PipeVerifyCorrupt = "GetRenderStateParameters"; + MGPipeFillForVerb(MGPipeVerb::DrawArrays); + MGPipeFillForVerb(MGPipeVerb::DrawElements); + ::_exit(0); + }); + ASSERT_TRUE(ExitedWith(r, 0)) << DescribeStatus(r) << "\n" << r.Log; + EXPECT_NE(r.Log.find("MGPipe: verify armed - 63 fields, 69 verbs, fatal=0"), std::string::npos) << r.Log; + const std::string first = "Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@DrawArrays\", verb=" + + std::to_string(serial) + ", where=entry}"; + const std::string second = "Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@DrawElements\", verb=" + + std::to_string(serial + 1) + ", where=entry}"; + EXPECT_NE(r.Log.find(first), std::string::npos) << "expected " << first << " in\n" << r.Log; + EXPECT_NE(r.Log.find(second), std::string::npos) << "expected " << second << " in\n" << r.Log; +#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) { @@ -254,9 +481,11 @@ TEST_F(PipeInputsTest, EveryVerbFillsItsClassAndNothingElse) { 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*. + // write, and caches the FILE*. The name carries this process's pid (see the file header), + // and the file is removed on the way out; a forked child that aborts leaves it to us. namespace fs = std::filesystem; - const fs::path path = fs::temp_directory_path() / "mobilegl-pipeinputs-test.log"; + const fs::path path = + fs::temp_directory_path() / ("mobilegl-pipeinputs-test-" + std::to_string(ProcessId()) + ".log"); std::error_code ec; fs::remove(path, ec); g_logPath = path.string(); @@ -266,5 +495,7 @@ int main(int argc, char** argv) { setenv("MOBILEGL_LOG_FILE_PATH", g_logPath.c_str(), 1); #endif ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + const int rc = RUN_ALL_TESTS(); + fs::remove(path, ec); + return rc; }