mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Test] (Pipe): the integration-verify lanes and their two always-on negative controls
- ARCHITECTURE.md 13.2-(2) asks for a third CI mode, and a third mode whose only evidence is "ctest was green" proves nothing: MOBILEGL_PIPE_VERIFY=1 against a library that never compiled the comparator in is a silent no-op that looks exactly like a clean pass. Six registrations, all under if (MOBILEGL_PIPE_VERIFY) and all labelled integration-verify, make both halves falsifiable - a mis-configured build registers nothing and --no-tests=error reds the lane, and PipeVerifyArmingScenario.Armed fails a lane whose library never printed its arming line. - PipeVerifyArmingScenario.CorruptedFieldIsReported is negative control A (G4): its lane pins MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters with MOBILEGL_PIPE_VERIFY_FATAL=0 so the process survives its own divergence and can read the report back; the CI step that exports the same knob against the ambient lane, where FATAL keeps its default, asserts the other half - the abort. - PoisonOmissionScenario is negative control B (G5), in two cases that cannot share a process because the knob is process-wide: the omitted (verb, field) pair must abort the glGenerateMipmap and NOT the draw before it, and the same sequence with the knob unset must complete with no Fatal at all. - The sequence runs in a fork()+execve() of this same binary rather than a bare fork(): the fixture has already brought a context up, and a bare fork of a process holding a live Vulkan device inherits the driver's mutexes with no threads to release them - measured here as a 120s wedge on DirectVulkan against a clean pass on DirectGLES. The child gets its own MOBILEGL_LOG_FILE_PATH because the library opens its log with fopen(path, "w") and would otherwise truncate the file the parent is about to read. - No ambient Verify. entry names MOBILEGL_PIPE_VERIFY_CORRUPT or MOBILEGL_PIPE_POISON_OMIT in its ENVIRONMENT property, because a property entry overrides the job environment for the names it lists: the two CI negative-control steps export those knobs into the job environment and must reach the processes. Every list appends MGL_ITEST_COMMON_ENV / MGL_ITEST_VULKAN_ENV for the same reason, so the vendor and ICD pinning survives.
This commit is contained in:
@@ -127,6 +127,8 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp
|
||||
Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
Scenarios/DualSourceBlendScenario.cpp
|
||||
Scenarios/PipeVerifyArmingScenario.cpp
|
||||
Scenarios/PoisonOmissionScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
@@ -631,3 +633,135 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_POINT_SIZE_DEMOTION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# --- the third CI mode: MOBILEGL_PIPE_VERIFY -----------------------------------
|
||||
#
|
||||
# ARCHITECTURE.md 13.2-(2) asks for a THIRD build mode next to pull and push: two state models in
|
||||
# one address space, compared field by field at every verb boundary and again at every accessor
|
||||
# read, 5-10x slower and never shipped. These entries are that mode's lane. They exist only when
|
||||
# the library was configured with -DMOBILEGL_PIPE_VERIFY=ON, which is deliberate and is half of
|
||||
# what makes the lane falsifiable: `ctest -L integration-verify --no-tests=error` in a build that
|
||||
# forgot the option matches NO tests and fails, instead of reporting a green run of nothing.
|
||||
#
|
||||
# The other half is PipeVerifyArmingScenario.Armed, which asserts the library's own arming line -
|
||||
# because MOBILEGL_PIPE_VERIFY=1 in the environment of a library that never compiled the
|
||||
# comparator in is a silent no-op that looks exactly like a clean pass.
|
||||
#
|
||||
# Three things about the ENVIRONMENT properties below, each of which has already gone wrong once
|
||||
# in this file:
|
||||
# * every list APPENDS ${MGL_ITEST_COMMON_ENV} / ${MGL_ITEST_VULKAN_ENV}. A ctest ENVIRONMENT
|
||||
# entry overrides the job environment for the names it lists, so an entry that named only its
|
||||
# own knobs would lose the EGL vendor and Vulkan ICD pinning and run against whichever driver
|
||||
# the loader found first.
|
||||
# * the ambient Verify. entries name NEITHER MOBILEGL_PIPE_VERIFY_CORRUPT NOR
|
||||
# MOBILEGL_PIPE_POISON_OMIT. That is what lets CI's two always-on negative-control steps
|
||||
# export those knobs in the JOB environment and have them reach the test processes; a
|
||||
# property entry of the same name would silently win and the controls would prove nothing.
|
||||
# * MOBILEGL_LOG_FILE_PATH is per lane. It is the only channel a test process has for reading
|
||||
# the library's own report (MG_Config is not reachable from this module), and the log is
|
||||
# opened with fopen(path, "w"), so each process truncates it and the cases can trust it.
|
||||
if (MOBILEGL_PIPE_VERIFY)
|
||||
# 900s, not the ambient 120: the comparator re-reads every field of the fill mask at the verb
|
||||
# boundary and again at every accessor read, which the design budgets at 5-10x.
|
||||
set(MGL_ITEST_VERIFY_TIMEOUT 900)
|
||||
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectGLES.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-DirectVulkan.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
# Negative control A (G4). MOBILEGL_PIPE_VERIFY_FATAL=0 so the process SURVIVES its own
|
||||
# divergence and the case can read the report back out of the log; the CI step that exports
|
||||
# the same corruption against the ambient lane, where FATAL keeps its default of 1, asserts
|
||||
# the other half - that a divergence aborts and reds the entry.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_VERIFY_CORRUPT_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters" "MOBILEGL_PIPE_VERIFY_FATAL=0"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectGLES.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_VERIFY_CORRUPT_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters" "MOBILEGL_PIPE_VERIFY_FATAL=0"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-corrupt-DirectVulkan.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
# Negative control B (G5). The omission skips the STAMP of one field for one verb while still
|
||||
# copying its value, which is indistinguishable from a fill row nobody wrote; the scenario
|
||||
# forks, so the resulting std::abort() is a datum in waitpid() rather than a dead lane.
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectGLES.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_PIPE_VERIFY=1"
|
||||
"MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-poison-omit-DirectVulkan.log"
|
||||
${MGL_ITEST_VULKAN_ENV})
|
||||
|
||||
# The whole suite again, per backend, with the comparator armed. Same scenarios, same
|
||||
# assertions, but every backend read of frontend state is now checked against a snapshot taken
|
||||
# from the live context at the verb boundary - which is what "the 742 integration entries
|
||||
# prove push equals pull" means. Labelled integration-gpu as well so a verify build's
|
||||
# `ctest -L integration-gpu` still describes the whole registration set.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.Verify."
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_VERIFY_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.Verify."
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_VERIFY_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# One case each: the knobs are process-wide, so a corrupted or poisoned process cannot also be
|
||||
# running the ambient assertions. These four entries are the ones that assert the RED - they
|
||||
# pass when the comparator and the poison report, and go red when either stops.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.VerifyCorrupted."
|
||||
TEST_FILTER "PipeVerifyArmingScenario.CorruptedFieldIsReported"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_VERIFY_CORRUPT_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.VerifyCorrupted."
|
||||
TEST_FILTER "PipeVerifyArmingScenario.CorruptedFieldIsReported"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_VERIFY_CORRUPT_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.PoisonOmitted."
|
||||
TEST_FILTER "PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_POISON_OMIT_ENVIRONMENT}"
|
||||
)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.PoisonOmitted."
|
||||
TEST_FILTER "PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS "integration-gpu\;integration-verify"
|
||||
TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_VULKAN_POISON_OMIT_ENVIRONMENT}"
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PipeVerifyArmingScenario.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
|
||||
//
|
||||
// Scenario - THE MOBILEGL_PIPE_VERIFY COMPARATOR IS ARMED, AND SAYS SO, AND CAN GO RED.
|
||||
//
|
||||
// The third CI mode (ARCHITECTURE.md 13.2-(2)) runs the whole integration suite with two state
|
||||
// models in one address space: the PipeInputs block the frontend fills at every verb boundary,
|
||||
// and a SnapshotFromGLContext() taken from the live GLContext. A green run of that mode is only
|
||||
// worth something if the comparator was actually RUNNING - and "MOBILEGL_PIPE_VERIFY=1 against a
|
||||
// library that was not built with -DMOBILEGL_PIPE_VERIFY=ON" is a no-op that looks exactly like a
|
||||
// clean pass. That is the failure mode this scenario exists to make impossible:
|
||||
//
|
||||
// Armed - the environment says the comparator is on for this process, so the
|
||||
// library must SAY it armed. It asserts a library observable against
|
||||
// the environment, the same shape UnlocatedIoBlockScenario's arming
|
||||
// case and AsyncCompileScenario::ExtensionStringMatchesTheConfiguration
|
||||
// use. A lane whose library never armed FAILS here; it never passes.
|
||||
// CorruptedFieldIsReported - the negative control for the comparator itself (gate G4). With
|
||||
// MOBILEGL_PIPE_VERIFY_CORRUPT naming a field, the snapshot arm is
|
||||
// perturbed before the entry compare, so a comparator that works must
|
||||
// report Fatal{PipeVerifyDiffer, "<Field>@<Verb>"}. A comparator that
|
||||
// compares nothing stays quiet and this case goes red.
|
||||
//
|
||||
// The observable is the library's own log, because MG_Config is not reachable from this module
|
||||
// (on Android it links the SHIPPING libMobileGL.so, built -fvisibility=hidden) and the arming
|
||||
// signal is a latched MGLOG_I. The ctest entry sets MOBILEGL_LOG_FILE_PATH; this only reads it.
|
||||
//
|
||||
// Note on scope: the log file is opened with fopen(path, "w") at the first log write of a process
|
||||
// (MG_Util/Debug/Log.cpp, InitFile), so the file holds THIS process's lines and nothing else - a
|
||||
// whole-file search cannot be satisfied by a sibling ctest entry of the same lane. The arming line
|
||||
// is latched at the FIRST fill of the process, which may be the harness bring-up rather than this
|
||||
// test's draw, so the arming search is whole-file on purpose; the divergence search is restricted
|
||||
// to the bytes this case appended, which is where a differ belongs.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// The three strings the comparator contracts to print (the brief's D8 reporting shape).
|
||||
// They are spelled here once so a rename of either half is one compile-visible edit.
|
||||
constexpr const char* kArmedLine = "MGPipe: verify armed";
|
||||
constexpr const char* kDifferPrefix = "Fatal{PipeVerifyDiffer";
|
||||
constexpr const char* kUnmigratedPrefix = "Fatal{UnmigratedPipeInput";
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
|
||||
)";
|
||||
|
||||
// Reads the environment the way MG_ConfigLoader does (ScenarioFixture.h documents the
|
||||
// rule); a string knob is "set" when it is present and non-empty, which is exactly what
|
||||
// MG_ConfigLoader's QueryEnvVariable turns into a non-empty Features member.
|
||||
bool StringKnobIsSet(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && *value != '\0';
|
||||
}
|
||||
|
||||
class PipeVerifyArmingScenario : public ScenarioTest {
|
||||
protected:
|
||||
// The library log this process is writing, or an empty path when none was configured.
|
||||
static std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
static std::uintmax_t LibraryLogSize() {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return 0;
|
||||
const std::uintmax_t size = std::filesystem::file_size(path, ec);
|
||||
return ec ? 0 : size;
|
||||
}
|
||||
|
||||
static std::string LibraryLogSince(std::uintmax_t offset) {
|
||||
const std::filesystem::path path = LibraryLogPath();
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
file.seekg(static_cast<std::streamoff>(offset));
|
||||
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
static std::string LibraryLog() { return LibraryLogSince(0); }
|
||||
|
||||
// One frame that crosses several verb boundaries: a clear (kClear), a draw (kDraw) and
|
||||
// a readback (kReadback). Three of the nine fill classes, so an entry compare that only
|
||||
// ran for one of them still has something to say.
|
||||
void DrawOneFrame() {
|
||||
HeadlessGL& gl = Gl();
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, gl.Width(), gl.Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
Rgba8 pixel{};
|
||||
glReadPixels(gl.Width() / 2, gl.Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel);
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
m_centre = pixel;
|
||||
}
|
||||
|
||||
Rgba8 m_centre{};
|
||||
};
|
||||
|
||||
// THE CASE THAT FAILS A LANE WHOSE LIBRARY NEVER ARMED.
|
||||
//
|
||||
// Every other entry in the integration-verify lane renders the same frames it renders in the
|
||||
// ambient lane and would be just as green against a library with no comparator compiled in -
|
||||
// which is precisely how a verify lane goes green having verified nothing. This case is the
|
||||
// one that cannot: the environment pins MOBILEGL_PIPE_VERIFY=1, therefore the library must
|
||||
// have said "MGPipe: verify armed" in its own log, and if it did not, the mode is not running.
|
||||
TEST_F(PipeVerifyArmingScenario, Armed) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "this case needs MOBILEGL_PIPE_VERIFY=1 for the whole process, which is "
|
||||
"what the Verify. ctest entries set; with the variable unset the "
|
||||
"comparator is dormant even in a build that compiled it in";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY is pinned on but MOBILEGL_LOG_FILE_PATH is not "
|
||||
"set, so the library has nowhere to record that it armed; the Verify. "
|
||||
"ctest entries set both";
|
||||
}
|
||||
if (StringKnobIsSet("MOBILEGL_PIPE_VERIFY_CORRUPT")) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY_CORRUPT is armed in this process, so a divergence "
|
||||
"is the EXPECTED outcome and asserting on its absence here would be "
|
||||
"backwards; the VerifyCorrupted. lane owns that half";
|
||||
}
|
||||
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
ASSERT_NO_FATAL_FAILURE(DrawOneFrame());
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
// Whole file, not just the appended bytes: the arming line is latched at the FIRST fill
|
||||
// of the process, which may already have happened during the harness bring-up. The file
|
||||
// is truncated at this process's first log write, so it still carries nothing else.
|
||||
const std::string whole = LibraryLog();
|
||||
EXPECT_NE(whole.find(kArmedLine), std::string::npos)
|
||||
<< "MOBILEGL_PIPE_VERIFY=1 is set for this process and a frame was cleared, drawn and "
|
||||
"read back, and the library never reported arming the comparator. Either this "
|
||||
"library was not built with -DMOBILEGL_PIPE_VERIFY=ON (in which case the whole lane "
|
||||
"is verifying nothing), or the arming MGLOG_I is gone. Log:\n"
|
||||
<< whole;
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
EXPECT_EQ(appended.find(kDifferPrefix), std::string::npos)
|
||||
<< "the comparator reported a push/pull divergence on an ordinary frame:\n"
|
||||
<< appended;
|
||||
EXPECT_EQ(appended.find(kUnmigratedPrefix), std::string::npos)
|
||||
<< "a backend read a field the verb's fill table does not list (add the row to "
|
||||
"MG_Pipe/FillPoints.def, never mark the field sticky):\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
// NEGATIVE CONTROL A (gate G4): a deliberately corrupted snapshot field must turn a green
|
||||
// verify run red, naming that field and the verb it diverged on.
|
||||
//
|
||||
// It runs in its own lane (VerifyCorrupted.) because the knob is process-wide, and with
|
||||
// MOBILEGL_PIPE_VERIFY_FATAL=0 so the process survives its own divergence and this case can
|
||||
// read the report back out of the log. The CI step that runs the SAME knob against the
|
||||
// ambient lane - where FATAL keeps its default - asserts the other half: there, the
|
||||
// divergence must abort and ctest must go red.
|
||||
TEST_F(PipeVerifyArmingScenario, CorruptedFieldIsReported) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (!StringKnobIsSet("MOBILEGL_PIPE_VERIFY_CORRUPT")) {
|
||||
GTEST_SKIP() << "this case is the comparator's negative control and needs "
|
||||
"MOBILEGL_PIPE_VERIFY_CORRUPT=<FieldName> for the whole process, which "
|
||||
"is what the VerifyCorrupted. ctest entries set";
|
||||
}
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_VERIFY_CORRUPT is set but MOBILEGL_PIPE_VERIFY is not, so "
|
||||
"the comparator is dormant and there is nothing to corrupt";
|
||||
}
|
||||
if (LibraryLogPath().empty()) {
|
||||
GTEST_SKIP() << "MOBILEGL_LOG_FILE_PATH is not set, so the library has nowhere to report "
|
||||
"the divergence; the VerifyCorrupted. ctest entries set both";
|
||||
}
|
||||
|
||||
const std::string knob = std::getenv("MOBILEGL_PIPE_VERIFY_CORRUPT");
|
||||
const std::uintmax_t before = LibraryLogSize();
|
||||
ASSERT_NO_FATAL_FAILURE(DrawOneFrame());
|
||||
|
||||
const std::string appended = LibraryLogSince(before);
|
||||
const std::string expected = std::string(kDifferPrefix) + ", \"" + knob + "@";
|
||||
EXPECT_NE(appended.find(expected), std::string::npos)
|
||||
<< "MOBILEGL_PIPE_VERIFY_CORRUPT=" << knob
|
||||
<< " perturbs that field in the snapshot arm before every entry compare, so a working "
|
||||
"comparator must have reported " << expected << "...\". It reported nothing, which "
|
||||
"means the comparator is not comparing - and every green entry in this lane is "
|
||||
"green for no reason. Log appended by this case:\n"
|
||||
<< appended;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -0,0 +1,385 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PoisonOmissionScenario.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
|
||||
//
|
||||
// Scenario - NEGATIVE CONTROL B (gate G5): AN OMITTED FILL POINT ABORTS ON THAT VERB, AND ONLY THERE.
|
||||
//
|
||||
// The per-verb poison is the half of P1 that makes a forgotten fill row loud instead of silent: the
|
||||
// filler stamps a generation on every field it copies for a verb, and an accessor whose stamp is not
|
||||
// this verb's aborts with Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}. A mechanism that can only be
|
||||
// observed when someone forgets a row is a mechanism nobody can trust, so MOBILEGL_PIPE_POISON_OMIT
|
||||
// forges the mistake on purpose: it names one (verb, field) pair whose STAMP the filler skips while
|
||||
// still copying the value, which is indistinguishable from a row that was never written.
|
||||
//
|
||||
// The scenario asserts both halves of "on THAT verb, and only there":
|
||||
//
|
||||
// OmittedFieldAbortsOnThatVerb - with MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit,
|
||||
// a draw must still complete (GetActiveTextureUnit is not in kDraw's
|
||||
// mask, and the draw's own fields are stamped normally) and the
|
||||
// following glGenerateMipmap must abort naming exactly that pair.
|
||||
// WithoutOmissionCompletes - the identical sequence with the knob unset runs to completion with
|
||||
// no Fatal at all. Without this half, "it aborted" would say nothing
|
||||
// about WHY: a poison that fired on every verb would look just as red.
|
||||
//
|
||||
// The knob is process-wide, so the two cases cannot share a lane: the first runs in the PoisonOmitted.
|
||||
// entries, the second in the ambient Verify. entries (it skips when the knob IS set).
|
||||
//
|
||||
// WHY THE SEQUENCE RUNS IN A SEPARATE PROCESS, AND WHY THAT PROCESS IS fork()+execve() AND NOT fork()
|
||||
// ALONE. The poison reports with MGLOG_F and then std::abort(), in the middle of a GL command - so the
|
||||
// sequence cannot run in the test process, and the harness's own bring-up pre-flight
|
||||
// (Harness/HeadlessGL.cpp) already establishes the shape: run it where a SIGABRT is a datum in
|
||||
// waitpid() instead of a dead lane. But that pre-flight forks BEFORE any context exists, and this case
|
||||
// cannot: the fixture has already brought one up. A bare fork() of a process holding a live Vulkan
|
||||
// device inherits the driver's mutexes with no threads to release them, and the child wedges on its
|
||||
// first submit - measured here as a 120s timeout on DirectVulkan and a clean pass on DirectGLES, which
|
||||
// is exactly the kind of backend-shaped flake a control must not have. So the child immediately
|
||||
// execve()s a fresh copy of this same test binary, filtered to the worker case below, which brings up
|
||||
// its own context from scratch and knows nothing about the parent's.
|
||||
//
|
||||
// The child gets its OWN MOBILEGL_LOG_FILE_PATH for the same reason: the library opens its log with
|
||||
// fopen(path, "w"), so a child sharing the parent's path would truncate the file the parent is about
|
||||
// to read.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__ANDROID__) && __has_include(<sys/wait.h>)
|
||||
#define MGITEST_POISON_HAVE_FORK 1
|
||||
#include <csignal>
|
||||
#include <ctime>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
extern char** environ;
|
||||
#else
|
||||
#define MGITEST_POISON_HAVE_FORK 0
|
||||
#endif
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
// What the PoisonOmitted. ctest entry and the CI negative-control step name. The pair is
|
||||
// spelled here so the assertion below is about the exact string the poison contracts to
|
||||
// print (ARCHITECTURE.md 9.2: Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}).
|
||||
constexpr const char* kOmittedVerb = "GenerateMipmap";
|
||||
constexpr const char* kOmittedField = "GetActiveTextureUnit";
|
||||
constexpr const char* kFatalPrefix = "Fatal{UnmigratedPipeInput";
|
||||
|
||||
// Set only in the re-executed child, so the worker case below runs in that process and skips
|
||||
// everywhere else (including in the ambient lanes, where it is registered like any other case).
|
||||
constexpr const char* kChildMarker = "MGITEST_POISON_OMISSION_CHILD";
|
||||
constexpr const char* kWorkerFilter =
|
||||
"--gtest_filter=PoisonOmissionScenario.TheSequenceThePoisonControlsRun";
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.25, 0.5, 0.75, 1.0); }
|
||||
)";
|
||||
|
||||
bool StringKnobIsSet(const char* name) {
|
||||
const char* value = std::getenv(name);
|
||||
return value != nullptr && *value != '\0';
|
||||
}
|
||||
|
||||
std::filesystem::path LibraryLogPath() {
|
||||
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
||||
return (path != nullptr && *path != '\0') ? std::filesystem::path(path)
|
||||
: std::filesystem::path();
|
||||
}
|
||||
|
||||
// Where the child is told to write ITS log. Empty when the lane configured no log path at
|
||||
// all, in which case the signal is the only evidence and the text assertions are skipped.
|
||||
std::string ChildLogPath() {
|
||||
const std::filesystem::path parent = LibraryLogPath();
|
||||
if (parent.empty()) return {};
|
||||
return (parent.string() + ".poison-child");
|
||||
}
|
||||
|
||||
std::string ReadWholeFile(const std::string& path) {
|
||||
if (path.empty()) return {};
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.good()) return {};
|
||||
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
class PoisonOmissionScenario : public ScenarioTest {
|
||||
protected:
|
||||
// The sequence under test. Deliberately in this order: the DRAW comes first and must
|
||||
// survive - if the poison fired there, the "only that verb" half would be false and the
|
||||
// SIGABRT the parent waits for would prove nothing.
|
||||
void RunSequence() {
|
||||
HeadlessGL& gl = Gl();
|
||||
|
||||
std::string error;
|
||||
const unsigned int program = CompileProgram(kVS, kFS, &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
|
||||
// A two-level texture, so glGenerateMipmap has real work to do and cannot be
|
||||
// short-circuited into a no-op by a backend that inspects the level count first.
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
unsigned char pixels[8 * 8 * 4];
|
||||
for (std::size_t i = 0; i < sizeof(pixels); ++i) {
|
||||
pixels[i] = static_cast<unsigned char>(i);
|
||||
}
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 3);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
|
||||
static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
glGenBuffers(1, &vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, gl.Width(), gl.Height());
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glUseProgram(program);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
glFinish();
|
||||
std::fprintf(stderr, "[itest] poison worker: the draw completed\n");
|
||||
|
||||
// The verb the omission names. Under MOBILEGL_PIPE_POISON_OMIT this must abort.
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
glFinish();
|
||||
std::fprintf(stderr, "[itest] poison worker: glGenerateMipmap returned\n");
|
||||
|
||||
glBindVertexArray(0);
|
||||
glDeleteBuffers(1, &vbo);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
|
||||
#if MGITEST_POISON_HAVE_FORK
|
||||
// fork() + execve() of this same binary, filtered to the worker case, with the marker and
|
||||
// the child's own log path added to the environment. Everything that allocates happens
|
||||
// BEFORE the fork; between fork and execve only async-signal-safe work is done.
|
||||
static bool RunSequenceInAChildProcess(int& outStatus, std::string& outReason) {
|
||||
std::vector<std::string> env;
|
||||
for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) {
|
||||
const std::string text(*entry);
|
||||
if (text.rfind("MOBILEGL_LOG_FILE_PATH=", 0) == 0) continue;
|
||||
if (text.rfind(std::string(kChildMarker) + "=", 0) == 0) continue;
|
||||
env.push_back(text);
|
||||
}
|
||||
env.push_back(std::string(kChildMarker) + "=1");
|
||||
const std::string childLog = ChildLogPath();
|
||||
if (!childLog.empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(childLog, ec);
|
||||
env.push_back("MOBILEGL_LOG_FILE_PATH=" + childLog);
|
||||
}
|
||||
|
||||
std::vector<char*> envp;
|
||||
envp.reserve(env.size() + 1);
|
||||
for (std::string& entry : env) envp.push_back(entry.data());
|
||||
envp.push_back(nullptr);
|
||||
|
||||
std::string exe = "/proc/self/exe";
|
||||
std::string arg0 = "MobileGLIntegrationTest";
|
||||
std::string filter = kWorkerFilter;
|
||||
char* argv[] = {arg0.data(), filter.data(), nullptr};
|
||||
|
||||
std::fflush(nullptr);
|
||||
const pid_t child = fork();
|
||||
if (child < 0) {
|
||||
outReason = "fork() failed";
|
||||
return false;
|
||||
}
|
||||
if (child == 0) {
|
||||
execve(exe.c_str(), argv, envp.data());
|
||||
// execve only returns on failure; _exit, never exit(), because every atexit
|
||||
// handler in this address space belongs to the parent's copy of the world.
|
||||
std::fprintf(stderr, "[itest] poison child: execve(/proc/self/exe) failed\n");
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
constexpr int kTimeoutMs = 120000;
|
||||
int waitedMs = 0;
|
||||
for (;;) {
|
||||
const pid_t reaped = waitpid(child, &outStatus, WNOHANG);
|
||||
if (reaped == child) return true;
|
||||
if (reaped < 0) {
|
||||
outReason = "waitpid on the poison worker failed";
|
||||
return false;
|
||||
}
|
||||
if (waitedMs >= kTimeoutMs) {
|
||||
kill(child, SIGKILL);
|
||||
(void)waitpid(child, &outStatus, 0);
|
||||
outReason = "the poison worker made no progress in 120s and was killed";
|
||||
return false;
|
||||
}
|
||||
timespec nap{0, 10 * 1000 * 1000};
|
||||
nanosleep(&nap, nullptr);
|
||||
waitedMs += 10;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string DescribeStatus(int status) {
|
||||
if (WIFEXITED(status)) return "exited with status " + std::to_string(WEXITSTATUS(status));
|
||||
if (WIFSIGNALED(status)) return "died on signal " + std::to_string(WTERMSIG(status));
|
||||
return "ended in an unrecognised way";
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
// The worker. It is a normal registered case so that the re-executed child can be selected
|
||||
// with nothing but --gtest_filter, and it skips in every process that is not that child.
|
||||
TEST_F(PoisonOmissionScenario, TheSequenceThePoisonControlsRun) {
|
||||
if (std::getenv(kChildMarker) == nullptr) {
|
||||
GTEST_SKIP() << "this case is the body the two poison controls run in a child process; "
|
||||
"it does nothing unless " << kChildMarker << " is set, which only the "
|
||||
"re-exec below does";
|
||||
}
|
||||
if (!Ready()) return;
|
||||
|
||||
RunSequence();
|
||||
|
||||
#if MGITEST_POISON_HAVE_FORK
|
||||
// _exit, and not a return into gtest's teardown: this process exists to reach the verb
|
||||
// above and its exit status is the datum the parent reads. A normal teardown of a live
|
||||
// context could add signals of its own to that answer.
|
||||
std::fflush(nullptr);
|
||||
_exit(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MGITEST_POISON_HAVE_FORK
|
||||
|
||||
TEST_F(PoisonOmissionScenario, OmittedFieldAbortsOnThatVerb) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (!StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT")) {
|
||||
GTEST_SKIP() << "this case is the poison's negative control and needs "
|
||||
"MOBILEGL_PIPE_POISON_OMIT=<Verb>:<Field> for the whole process, which "
|
||||
"is what the PoisonOmitted. ctest entries set";
|
||||
}
|
||||
const std::string knob = std::getenv("MOBILEGL_PIPE_POISON_OMIT");
|
||||
const std::string expectedPair = std::string(kOmittedField) + "@" + kOmittedVerb;
|
||||
if (knob != std::string(kOmittedVerb) + ":" + kOmittedField) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_POISON_OMIT is " << knob << ", but this case only knows "
|
||||
<< "how to provoke " << kOmittedVerb << ":" << kOmittedField;
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
std::string reason;
|
||||
ASSERT_TRUE(RunSequenceInAChildProcess(status, reason)) << reason;
|
||||
|
||||
const std::string childLog = ReadWholeFile(ChildLogPath());
|
||||
ASSERT_TRUE(WIFSIGNALED(status))
|
||||
<< "with the stamp of " << expectedPair << " omitted, the glGenerateMipmap in the child "
|
||||
<< "had to read a field its verb never filled and abort. It " << DescribeStatus(status)
|
||||
<< " instead - the poison is not armed (a build without MOBILEGL_PIPE_POISON, a filler "
|
||||
"that stamps what it was told to skip, or a backend that no longer reads the field "
|
||||
"through the accessor). Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(WTERMSIG(status), SIGABRT)
|
||||
<< "the child died on signal " << WTERMSIG(status) << " rather than SIGABRT; the poison "
|
||||
"reports through MGLOG_F + std::abort(), so any other signal is a different crash. "
|
||||
"Child log:\n"
|
||||
<< childLog;
|
||||
|
||||
if (ChildLogPath().empty()) {
|
||||
GTEST_SKIP() << "the abort happened, but the lane set no MOBILEGL_LOG_FILE_PATH, so the "
|
||||
"Fatal's text cannot be read back; the PoisonOmitted. ctest entries set it";
|
||||
}
|
||||
EXPECT_NE(childLog.find(std::string(kFatalPrefix) + ", \"" + expectedPair + "\""),
|
||||
std::string::npos)
|
||||
<< "the child aborted, but not with Fatal{UnmigratedPipeInput, \"" << expectedPair
|
||||
<< "\"} - that message is the whole diagnostic value of the poison. Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(childLog.find("@DrawArrays"), std::string::npos)
|
||||
<< "the draw that ran BEFORE the omitted verb also tripped the poison, so the omission "
|
||||
"is not scoped to its verb: the fill classes are wrong, or the stamps are global. "
|
||||
"Child log:\n"
|
||||
<< childLog;
|
||||
}
|
||||
|
||||
// The sibling control, in the ambient Verify. lanes: the same sequence with the knob UNSET
|
||||
// must run to completion and log no Fatal at all.
|
||||
TEST_F(PoisonOmissionScenario, WithoutOmissionCompletes) {
|
||||
if (!Ready()) return;
|
||||
|
||||
if (AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") != AmbientQuirk::On) {
|
||||
GTEST_SKIP() << "the poison is only compiled into the push/verify builds; in an ordinary "
|
||||
"build there is nothing for this control to be a control OF";
|
||||
}
|
||||
if (StringKnobIsSet("MOBILEGL_PIPE_POISON_OMIT")) {
|
||||
GTEST_SKIP() << "MOBILEGL_PIPE_POISON_OMIT is armed for this process, so the abort is the "
|
||||
"EXPECTED outcome here; OmittedFieldAbortsOnThatVerb owns that half and "
|
||||
"runs in the PoisonOmitted. lane";
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
std::string reason;
|
||||
ASSERT_TRUE(RunSequenceInAChildProcess(status, reason)) << reason;
|
||||
|
||||
const std::string childLog = ReadWholeFile(ChildLogPath());
|
||||
ASSERT_TRUE(WIFEXITED(status))
|
||||
<< "with no omission armed, a draw followed by glGenerateMipmap must complete; the child "
|
||||
<< DescribeStatus(status)
|
||||
<< ". If it aborted, the poison is firing on a field the verb's fill table SHOULD list - "
|
||||
"add the row to MG_Pipe/FillPoints.def, never mark the field sticky. Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(WEXITSTATUS(status), 0) << "the child " << DescribeStatus(status)
|
||||
<< ". Child log:\n"
|
||||
<< childLog;
|
||||
EXPECT_EQ(childLog.find("Fatal{"), std::string::npos)
|
||||
<< "an unpoisoned run logged a Fatal:\n"
|
||||
<< childLog;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
TEST_F(PoisonOmissionScenario, OmittedFieldAbortsOnThatVerb) {
|
||||
GTEST_SKIP() << "the poison control needs fork()/execve()/waitpid() to observe a SIGABRT as "
|
||||
"a datum; this platform has none of them";
|
||||
}
|
||||
|
||||
TEST_F(PoisonOmissionScenario, WithoutOmissionCompletes) {
|
||||
GTEST_SKIP() << "the poison control needs fork()/execve()/waitpid() to observe a SIGABRT as "
|
||||
"a datum; this platform has none of them";
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
Reference in New Issue
Block a user