// 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 #include #include #include #include "HeadlessGL.h" namespace MGITest { // How a MOBILEGL_* quirk variable reads in THIS process's environment. // // A scenario that needs a non-default configuration takes it from here and skips // when the process it was launched into is not in that configuration, rather than // writing MG_Config::Features itself. Two reasons, and the second one decides it: // // - the feature table is an internal symbol. On Android this module links against // the SHIPPING libMobileGL.so - deliberately, so the on-device run validates the // real artifact - and that library is built -fvisibility=hidden, so nothing // internal is reachable from here at all. // - a quirk poked in-process is already too late for everything latched at // initialization: the compile pool and its threads, and the backend's advertised // extension list, which is built once from the configuration in force at first // use. The process-wide variable is the only spelling that covers the whole // configuration instead of the half of it that is still mutable afterwards. // // The reading rule is MG_ConfigLoader's, character for character (ConfigLoader.cpp, // QueryEnvQuirkOverride / IsTruthyValue): unset is Auto - device auto-detection or a // built-in default, i.e. a value only the implementation knows - a truthy value is // On, and anything else that IS set ("0", "false", "") is Off. enum class AmbientQuirk { Auto, On, Off }; inline AmbientQuirk AmbientQuirkFromEnvironment(const char* name) { const char* value = std::getenv(name); if (value == nullptr) return AmbientQuirk::Auto; std::string lowered(value); for (char& c : lowered) { c = static_cast(std::tolower(static_cast(c))); } if (lowered.empty() || lowered == "0" || lowered == "false") return AmbientQuirk::Off; return AmbientQuirk::On; } 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 (RequireHardwareGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) { // Only when hardware was asked for BY NAME. REQUIRE_GPU means "an // unusable harness is a failure, not a silent skip" - it is the // falsifiability switch, and CI is exactly where it belongs. But CI // runners have no GPU, so folding "must not be llvmpipe" into the // same switch made the CI lane unpassable by construction: the // scenarios pin backend draw logic, which a software rasterizer // executes just as faithfully. Landing on llvmpipe/lavapipe there is // the intended configuration, not a misconfiguration. A vendor pin // that must not silently degrade sets REQUIRE_HARDWARE_GPU. FAIL() << "MOBILEGL_ITEST_REQUIRE_HARDWARE_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