mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
[Test] (MG_IntegrationTest): pin the two shipped memo bugs with rendered pixels
Bothd7976326bugs passed every unit test while corrupting real frames - state-level assertions cannot see them. This module renders and reads back. A headless EGL-pbuffer harness (no window, no GLFW) linking MobileGL_s directly, registered once per backend under the ctest label integration-gpu, behind the default-OFF option MOBILEGL_BUILD_INTEGRATION_TEST. The platform pre-flight runs the ENTIRE bring-up in a forked child first - MobileGL aborts rather than returning errors on an unusable platform, and the child dying on any signal turns into a clean GTEST_SKIP instead of taking the test binary down. MOBILEGL_ITEST_REQUIRE_GPU makes the label falsifiable: with it set, an unusable harness (or a context that lands on a software rasterizer) is a FAILURE - without it, a CI runner whose driver pinning silently broke reports the same green as one that rendered every frame. Configure-time detection pins the EGL vendor and Vulkan ICD jsons, preferring hardware vendors and never selecting llvmpipe/lavapipe. Scenarios assert on glReadPixels with whole-region pixel counts (a 2x2 quadrant pattern whose signature distinguishes all eight square symmetries; every region predicate reports the first offending pixel): - OrientationScenario: default -> FBO -> default, pinning the transform-flags memo key. Keying GetBaseTransformFlagsRaw on the pre-transform alone fails exactly 3 entries. - StreamedArenaScenario: an untouched streamed vertex buffer must survive transient-arena recycling. Re-enabling only the cross-frame vertex revalidation fails exactly this entry. - CrossFrameBufferScenario + ResidentIndexScenario: cross-frame mutation matrix (SubData, map/unmap, persistent+flush, coherent persistent, orphan, CopyBufferSubData; vertex and index) plus six adversarial resident-EBO constructions. Instrumentation showed the cross-frame EBO memo cannot be made to serve wrong bytes from GL level on this stack (89 entries, 81 accepts, zero divergent slices) - these cases are freshness tripwires, documented as such in-file; the EBO half ofd7976326remains unpinned by a failing test. At the buggy commit72ee7c43the suite fails 4 entries (3 orientation + 1 streamed-arena); atd7976326all 52 pass, 5 consecutive runs, zero flakes, and the default build is bit-for-bit unaffected (unit suite unchanged). Adversarially verified twice, including hostile-platform sweeps (26 configurations, all clean skips) and hand-edits of each production hole in isolation.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h
|
||||
// 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 base fixture every scenario derives from. Its only jobs are to bring the
|
||||
// headless context up once per process and to decide what "this machine has no
|
||||
// usable GPU" means.
|
||||
//
|
||||
// By default it means a clean GTEST_SKIP() - never a failure, never a hang -
|
||||
// because a developer box or a container without a GPU should not fail a run it
|
||||
// was never able to perform. But a skip is indistinguishable from a pass in
|
||||
// every CI summary, so the `integration-gpu` label on its own is unfalsifiable:
|
||||
// a runner whose driver pinning silently broke reports the same green as one
|
||||
// that rendered every frame. MOBILEGL_ITEST_REQUIRE_GPU is the caller saying
|
||||
// "this machine HAS a GPU and I am relying on these scenarios actually running";
|
||||
// with it set, an unusable harness is a FAILURE carrying the pre-flight's reason.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "HeadlessGL.h"
|
||||
|
||||
namespace MGITest {
|
||||
|
||||
class ScenarioTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
m_ready = false;
|
||||
HeadlessGL& gl = HeadlessGL::Get();
|
||||
if (!gl.Usable()) {
|
||||
if (RequireGpu()) {
|
||||
// FAIL() is a FATAL failure but does NOT mark the test skipped,
|
||||
// so a derived SetUp that guards on IsSkipped() alone would run
|
||||
// straight into GL calls with no current context and SIGSEGV -
|
||||
// that exact crash shipped from the first version of this guard.
|
||||
// Derived fixtures must gate on Ready() (below), which is false
|
||||
// on BOTH the skip path and this failure path.
|
||||
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set, so an unusable harness is a failure, not a skip. "
|
||||
<< "Backend " << gl.BackendName() << " could not be brought up: " << gl.SkipReason();
|
||||
}
|
||||
GTEST_SKIP() << "no usable GPU/display/ICD for backend " << gl.BackendName() << ": " << gl.SkipReason();
|
||||
}
|
||||
if (RequireGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) {
|
||||
// "Ran on llvmpipe" must not be able to pass as "ran on the GPU":
|
||||
// a misconfigured vendor pin silently lands on the software
|
||||
// rasterizer, and REQUIRE_GPU exists precisely to make that loud.
|
||||
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set but the context landed on a software rasterizer: "
|
||||
<< gl.RendererString();
|
||||
}
|
||||
// A scenario starts from a clean slate but shares the context (and so
|
||||
// the renderer's memos) with every other scenario in this process -
|
||||
// which is exactly the situation both shipped bugs needed.
|
||||
RecordProperty("backend", gl.BackendName());
|
||||
RecordProperty("renderer", gl.RendererString());
|
||||
m_ready = true;
|
||||
}
|
||||
|
||||
// The ONLY gate a derived SetUp/TearDown may use: `if (!Ready()) return;`.
|
||||
// True only when the base SetUp brought the context up and neither skipped
|
||||
// nor failed. IsSkipped() alone is WRONG here (see the comment at FAIL()).
|
||||
bool Ready() const { return m_ready; }
|
||||
|
||||
static HeadlessGL& Gl() { return HeadlessGL::Get(); }
|
||||
|
||||
private:
|
||||
static bool LooksLikeSoftwareRasterizer(const std::string& renderer) {
|
||||
static const char* kNames[] = {"llvmpipe", "lavapipe", "softpipe", "SwiftShader", "swrast"};
|
||||
for (const char* name : kNames) {
|
||||
if (renderer.find(name) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool m_ready = false;
|
||||
};
|
||||
|
||||
} // namespace MGITest
|
||||
Reference in New Issue
Block a user