mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
[Fix, Feat, Test] (MG_Util, MG_Backend, MG_Impl, MG_State): correct the log severity ordering and unwind the diagnostics it silenced
This commit is contained in:
@@ -16,5 +16,25 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
# The log-severity ordering and the MOBILEGL_ASSERT gate keyed to it. Links gtest
|
||||
# (not gtest_main): the suite needs its own main() to point MOBILEGL_LOG_FILE_PATH at a
|
||||
# temp file before anything in the process logs and latches the sink's FILE*.
|
||||
add_executable(
|
||||
LogLevelTest
|
||||
LogLevelTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(LogLevelTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
LogLevelTest PRIVATE
|
||||
GTest::gtest
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(JobNodeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(LogLevelTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// MobileGL - MobileGL/MG_Test/Util/LogLevelTest.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
|
||||
|
||||
// Guards the log-severity ordering and the MOBILEGL_ASSERT gate that hangs off it.
|
||||
//
|
||||
// Until 2026-08-13 the numeric order was DEBUG < WARN < ERROR < INFO < FATAL, so the
|
||||
// production gate `#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_X` compiled
|
||||
// MGLOG_W and MGLOG_E out of every INFO build. Failures logged with MGLOG_E were
|
||||
// invisible in exactly the builds that shipped. Nothing in the suite noticed, which is
|
||||
// why this file exists.
|
||||
//
|
||||
// This test is written to be meaningful in BOTH configurations - build it at
|
||||
// MOBILEGL_LOG_LEVEL_INFO and at MOBILEGL_LOG_LEVEL_DEBUG and it checks the contract
|
||||
// appropriate to each. It runs headless: no GL context, no device, just the file sink.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <Defines.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compile-time contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// The ordering itself. A renumbering that re-inverts the scale fails here.
|
||||
static_assert(MOBILEGL_LOG_LEVEL_DEBUG < MOBILEGL_LOG_LEVEL_INFO, "DEBUG must be below INFO");
|
||||
static_assert(MOBILEGL_LOG_LEVEL_INFO < MOBILEGL_LOG_LEVEL_WARN, "INFO must be below WARN");
|
||||
static_assert(MOBILEGL_LOG_LEVEL_WARN < MOBILEGL_LOG_LEVEL_ERROR, "WARN must be below ERROR");
|
||||
static_assert(MOBILEGL_LOG_LEVEL_ERROR < MOBILEGL_LOG_LEVEL_FATAL, "ERROR must be below FATAL");
|
||||
|
||||
// DEBUG must stay the floor: the MOBILEGL_ASSERT gate in Defines.h is spelled
|
||||
// `ACTIVE <= MOBILEGL_LOG_LEVEL_DEBUG` and means "only in a DEBUG build". That
|
||||
// reading is only correct while DEBUG is the minimum.
|
||||
static_assert(MOBILEGL_LOG_LEVEL_DEBUG == 0, "DEBUG must be the lowest level");
|
||||
|
||||
// Defines.h and Log.h each define the five constants. Log.h's copy wins when both
|
||||
// are included; if the two ever drift, the duplicate-definition warning is not
|
||||
// guaranteed to be an error, so pin the values a second time from this TU's view.
|
||||
static_assert(MOBILEGL_LOG_LEVEL_INFO == 1, "INFO must be 1 in both Defines.h and Log.h");
|
||||
static_assert(MOBILEGL_LOG_LEVEL_WARN == 2, "WARN must be 2 in both Defines.h and Log.h");
|
||||
static_assert(MOBILEGL_LOG_LEVEL_ERROR == 3, "ERROR must be 3 in both Defines.h and Log.h");
|
||||
static_assert(MOBILEGL_LOG_LEVEL_FATAL == 4, "FATAL must be 4 in both Defines.h and Log.h");
|
||||
|
||||
// Whether this translation unit was compiled with asserts live. This is a literal
|
||||
// copy of the Defines.h gate - the point of the test is to prove it agrees with
|
||||
// MGLOG_D's liveness, observed at runtime below.
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
constexpr bool kAssertsLive = true;
|
||||
#else
|
||||
constexpr bool kAssertsLive = false;
|
||||
#endif
|
||||
|
||||
// Whether the build is the production INFO configuration.
|
||||
constexpr bool kBuiltAtInfo = (MOBILEGL_LOG_ACTIVE_LEVEL == MOBILEGL_LOG_LEVEL_INFO);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Runtime observation of the sink
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// The log file path is latched by MG_Util::Debug::InitFile() on the first write and
|
||||
// never reopened, so the whole process gets one file. main() below points
|
||||
// MOBILEGL_LOG_FILE_PATH at a temp file before gtest runs; this fixture emits one
|
||||
// line per level and then reads the file back.
|
||||
std::string g_logPath;
|
||||
|
||||
struct Emitted {
|
||||
bool debug = false;
|
||||
bool info = false;
|
||||
bool warn = false;
|
||||
bool error = false;
|
||||
bool fatal = false;
|
||||
};
|
||||
|
||||
Emitted EmitAndRead() {
|
||||
// Distinctive markers so a substring search cannot collide with unrelated output.
|
||||
MGLOG_D("MGLOGTEST_MARKER_DEBUG_5f3a");
|
||||
MGLOG_I("MGLOGTEST_MARKER_INFO_5f3a");
|
||||
MGLOG_W("MGLOGTEST_MARKER_WARN_5f3a");
|
||||
MGLOG_E("MGLOGTEST_MARKER_ERROR_5f3a");
|
||||
MGLOG_F("MGLOGTEST_MARKER_FATAL_5f3a");
|
||||
|
||||
std::ifstream in(g_logPath, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
const std::string text = ss.str();
|
||||
|
||||
Emitted e;
|
||||
e.debug = text.find("MGLOGTEST_MARKER_DEBUG_5f3a") != std::string::npos;
|
||||
e.info = text.find("MGLOGTEST_MARKER_INFO_5f3a") != std::string::npos;
|
||||
e.warn = text.find("MGLOGTEST_MARKER_WARN_5f3a") != std::string::npos;
|
||||
e.error = text.find("MGLOGTEST_MARKER_ERROR_5f3a") != std::string::npos;
|
||||
e.fatal = text.find("MGLOGTEST_MARKER_FATAL_5f3a") != std::string::npos;
|
||||
return e;
|
||||
}
|
||||
|
||||
TEST(LogLevel, SinkIsReachableAtAll) {
|
||||
// Guards the test itself: if the file sink were disabled or the path override
|
||||
// ignored, every "level X is suppressed" assertion below would pass vacuously.
|
||||
ASSERT_FALSE(g_logPath.empty()) << "test harness did not set MOBILEGL_LOG_FILE_PATH";
|
||||
const Emitted e = EmitAndRead();
|
||||
EXPECT_TRUE(e.fatal) << "FATAL is compiled in at every level; an empty log means the "
|
||||
"file sink never opened and this suite proves nothing";
|
||||
}
|
||||
|
||||
TEST(LogLevel, ProductionBuildKeepsErrorAndWarn) {
|
||||
if constexpr (!kBuiltAtInfo) {
|
||||
GTEST_SKIP() << "only meaningful when built at MOBILEGL_LOG_LEVEL_INFO";
|
||||
} else {
|
||||
const Emitted e = EmitAndRead();
|
||||
// The regression this file exists for.
|
||||
EXPECT_TRUE(e.error) << "MGLOG_E must be live in an INFO build";
|
||||
EXPECT_TRUE(e.warn) << "MGLOG_W must be live in an INFO build";
|
||||
EXPECT_TRUE(e.info) << "MGLOG_I must be live in an INFO build";
|
||||
EXPECT_TRUE(e.fatal) << "MGLOG_F must be live in an INFO build";
|
||||
// ...and the other half: D must still be compiled out, or production pays
|
||||
// for every dev-only diagnostic in the tree.
|
||||
EXPECT_FALSE(e.debug) << "MGLOG_D must be compiled out of an INFO build";
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LogLevel, DebugBuildKeepsEverything) {
|
||||
if constexpr (MOBILEGL_LOG_ACTIVE_LEVEL != MOBILEGL_LOG_LEVEL_DEBUG) {
|
||||
GTEST_SKIP() << "only meaningful when built at MOBILEGL_LOG_LEVEL_DEBUG";
|
||||
} else {
|
||||
const Emitted e = EmitAndRead();
|
||||
EXPECT_TRUE(e.debug);
|
||||
EXPECT_TRUE(e.info);
|
||||
EXPECT_TRUE(e.warn);
|
||||
EXPECT_TRUE(e.error);
|
||||
EXPECT_TRUE(e.fatal);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The assert contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(LogLevel, AssertGateTracksDebugLiveness) {
|
||||
// The contract: "INFO builds: asserts OFF; DEBUG builds: asserts ON". Stated
|
||||
// without naming a level, that is exactly "asserts are live iff MGLOG_D is
|
||||
// live" - which is checkable in whichever configuration this was built in,
|
||||
// and is what makes the renumbering safe.
|
||||
const Emitted e = EmitAndRead();
|
||||
EXPECT_EQ(kAssertsLive, e.debug)
|
||||
<< "MOBILEGL_ASSERT liveness (" << kAssertsLive << ") disagrees with MGLOG_D liveness ("
|
||||
<< e.debug << "). The Defines.h assert gate and the Log.h MGLOG_D gate have drifted.";
|
||||
}
|
||||
|
||||
TEST(LogLevel, AssertIsCompiledOutOfProductionBuilds) {
|
||||
if constexpr (kAssertsLive) {
|
||||
GTEST_SKIP() << "asserts are live in this configuration; see AssertIsLiveInDebugBuilds";
|
||||
} else {
|
||||
// If MOBILEGL_ASSERT were live here this would TRAP and take the process
|
||||
// down, which is the behavioural half of the contract.
|
||||
MOBILEGL_ASSERT(false, "this assert must be compiled out at %s", "INFO");
|
||||
SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LogLevel, AssertIsLiveInDebugBuilds) {
|
||||
if constexpr (!kAssertsLive) {
|
||||
GTEST_SKIP() << "asserts are compiled out in this configuration";
|
||||
} else {
|
||||
// A satisfied assert must be a no-op rather than a trap; that it expands to
|
||||
// real code at all is what kAssertsLive already established.
|
||||
MOBILEGL_ASSERT(true, "a satisfied assert must not trap");
|
||||
SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Must happen 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-loglevel-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