Files
TheRock/rocm-libraries/projects/hipdnn/tests/frontend/IntegrationFrontendUserLogging.cpp
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

565 lines
22 KiB
C++

// Copyright © Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <gtest/gtest.h>
#include <hip/hip_runtime.h>
#include <hipdnn_frontend.hpp>
#include <hipdnn_test_sdk/constants/ConvFpropConstants.hpp>
#include <hipdnn_test_sdk/utilities/LiftingTestHelpers.hpp>
#include <hipdnn_test_sdk/utilities/LogRecorder.hpp>
#include <hipdnn_test_sdk/utilities/TestUtilities.hpp>
#include <hipdnn_test_sdk/utilities/TestableGraph.hpp>
#include "test_plugins/TestPluginConstants.hpp"
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
constexpr auto ASYNC_LOG_TIMEOUT = std::chrono::seconds(10);
constexpr auto NEGATIVE_ASSERT_TIMEOUT = std::chrono::milliseconds(100);
using namespace hipdnn_frontend;
using namespace hipdnn_test_sdk::utilities;
class IntegrationFrontendUserLogging : public ::testing::Test
{
protected:
hipdnnSeverity_t _originalLogLevel = HIPDNN_SEV_OFF;
void SetUp() override
{
// Save original log level
auto error = getGlobalLogLevel(_originalLogLevel);
ASSERT_EQ(error.code, ErrorCode::OK);
// Clear any previous user callback (may fail if not registered — that's OK)
setIsolatedUserCallback(HIPDNN_SEV_OFF, LogCallbackMode::ASYNC);
// Reset log level
error = setGlobalLogLevel(HIPDNN_SEV_OFF);
ASSERT_EQ(error.code, ErrorCode::OK);
}
void TearDown() override
{
// Clear user callback (may fail if not registered — that's OK)
setIsolatedUserCallback(HIPDNN_SEV_OFF, LogCallbackMode::ASYNC);
// Restore original log level
auto error = setGlobalLogLevel(_originalLogLevel);
ASSERT_EQ(error.code, ErrorCode::OK);
}
// Set the isolated user callback. Returns the error without asserting.
Error setIsolatedUserCallback(hipdnnSeverity_t minLevel, LogCallbackMode mode)
{
return setUserLogCallback(
IsolatedLogRecorder::getIsolatedUserRecordingCallback(), minLevel, mode, this);
}
// Register/update/unregister the isolated user callback. Asserts success.
void registerUserCallback(hipdnnSeverity_t minLevel, LogCallbackMode mode)
{
ASSERT_EQ(setIsolatedUserCallback(minLevel, mode).code, ErrorCode::OK);
}
};
// Test: Frontend logs are produced on user callback
TEST_F(IntegrationFrontendUserLogging, FrontendLogsProducedOnUserCallback)
{
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
// Register user callback through frontend API
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::ASYNC);
auto error = setGlobalLogLevel(HIPDNN_SEV_INFO);
ASSERT_EQ(error.code, ErrorCode::OK);
// Emit frontend logs
HIPDNN_FE_LOG_INFO("Test info message from frontend");
HIPDNN_FE_LOG_WARN("Test warning message from frontend");
HIPDNN_FE_LOG_ERROR("Test error message from frontend");
// Wait for async callbacks to deliver all log messages
EXPECT_TRUE(recorder.waitForLogsContaining(
{"Test info message", "Test warning message", "Test error message"}, ASYNC_LOG_TIMEOUT))
<< "Timed out waiting for all expected log messages\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
// Verify logs were received on callback with correct severity
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_INFO, "Test info message"));
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_WARN, "Test warning message"));
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_ERROR, "Test error message"));
}
// Test: Log level API controls which frontend logs are produced
TEST_F(IntegrationFrontendUserLogging, LogLevelControlsFrontendLogs)
{
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::ASYNC);
// Set to WARN level - INFO should be filtered
auto error = setGlobalLogLevel(HIPDNN_SEV_WARN);
ASSERT_EQ(error.code, ErrorCode::OK);
HIPDNN_FE_LOG_INFO("Info should be filtered");
HIPDNN_FE_LOG_WARN("Warning should pass");
HIPDNN_FE_LOG_ERROR("Error should pass");
// Wait for expected logs (WARN + ERROR); INFO should be filtered
EXPECT_TRUE(recorder.waitForLogsContaining({"Warning should pass", "Error should pass"},
ASYNC_LOG_TIMEOUT))
<< "Timed out waiting for expected WARN and ERROR logs\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
// INFO filtered, WARN and ERROR pass
EXPECT_FALSE(recorder.hasLogContaining("Info should be filtered"));
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_WARN, "Warning should pass"));
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_ERROR, "Error should pass"));
}
// Test: Setting log level to OFF filters all logs
TEST_F(IntegrationFrontendUserLogging, LogLevelOffFiltersAllLogs)
{
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::ASYNC);
auto error = setGlobalLogLevel(HIPDNN_SEV_OFF);
ASSERT_EQ(error.code, ErrorCode::OK);
HIPDNN_FE_LOG_INFO("Should be filtered");
HIPDNN_FE_LOG_WARN("Should be filtered");
HIPDNN_FE_LOG_ERROR("Should be filtered");
// Negative assertion: no logs expected, bounded wait to confirm none arrive
EXPECT_FALSE(recorder.waitForLogCount(1, NEGATIVE_ASSERT_TIMEOUT))
<< "Unexpected log received despite log level OFF\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
// All logs filtered
EXPECT_EQ(recorder.getRecordedLogCount(), 0);
}
// Test: Unregistering callback with SEV_OFF stops log callbacks
TEST_F(IntegrationFrontendUserLogging, UnregisterWithSevOffStopsCallbacks)
{
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
// Set callback
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::ASYNC);
auto error = setGlobalLogLevel(HIPDNN_SEV_INFO);
ASSERT_EQ(error.code, ErrorCode::OK);
const auto countBefore = recorder.getRecordedLogCount();
HIPDNN_FE_LOG_INFO("Log before unregistering callback");
// Wait for at least 1 log to arrive via async callback
EXPECT_TRUE(recorder.waitForLogCount(countBefore + 1, ASYNC_LOG_TIMEOUT))
<< "Timed out waiting for async log delivery\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
EXPECT_GT(recorder.getRecordedLogCount(), 0);
// Unregister callback with SEV_OFF
registerUserCallback(HIPDNN_SEV_OFF, LogCallbackMode::ASYNC);
// Capture count after unregister — the only point where it's guaranteed
// that no more logs will be delivered via the callback
const size_t logsAfterUnregister = recorder.getRecordedLogCount();
HIPDNN_FE_LOG_INFO("Log after unregistering callback");
// Negative assertion: no new logs expected after unregister, bounded wait
EXPECT_FALSE(recorder.waitForLogCount(logsAfterUnregister + 1, NEGATIVE_ASSERT_TIMEOUT))
<< "Unexpected log received after unregistering callback\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
// No new logs should be provided via callback
EXPECT_EQ(recorder.getRecordedLogCount(), logsAfterUnregister);
}
// Test: Callback level filtering works independently of global level
TEST_F(IntegrationFrontendUserLogging, CallbackLevelFiltersIndependently)
{
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
// Register callback at WARN level (filters INFO)
registerUserCallback(HIPDNN_SEV_WARN, LogCallbackMode::ASYNC);
// Set global level to INFO (allows INFO through)
auto error = setGlobalLogLevel(HIPDNN_SEV_INFO);
ASSERT_EQ(error.code, ErrorCode::OK);
HIPDNN_FE_LOG_INFO("Info should be filtered by callback level");
HIPDNN_FE_LOG_WARN("Warning should pass");
HIPDNN_FE_LOG_ERROR("Error should pass");
// Wait for expected logs (WARN + ERROR); INFO filtered at callback level
EXPECT_TRUE(recorder.waitForLogsContaining({"Warning should pass", "Error should pass"},
ASYNC_LOG_TIMEOUT))
<< "Timed out waiting for expected WARN and ERROR logs\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
// INFO filtered at callback level, WARN and ERROR pass
EXPECT_FALSE(recorder.hasLogContaining("Info should be filtered"));
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_WARN, "Warning should pass"));
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_ERROR, "Error should pass"));
}
// Test: Sync callback executes immediately
TEST_F(IntegrationFrontendUserLogging, SyncCallbackExecutesImmediately)
{
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
// Register callback in SYNC mode
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::SYNC);
auto error = setGlobalLogLevel(HIPDNN_SEV_INFO);
ASSERT_EQ(error.code, ErrorCode::OK);
HIPDNN_FE_LOG_INFO("Sync callback message");
// No delay needed for sync - should be immediate
EXPECT_GT(recorder.getRecordedLogCount(), 0);
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_INFO, "Sync callback message"));
}
// Test: Async callback queues logs
TEST_F(IntegrationFrontendUserLogging, AsyncCallbackQueuesLogs)
{
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
// Register callback in ASYNC mode
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::ASYNC);
auto error = setGlobalLogLevel(HIPDNN_SEV_INFO);
ASSERT_EQ(error.code, ErrorCode::OK);
HIPDNN_FE_LOG_INFO("Async callback message");
// Wait for async callback to deliver the log
EXPECT_TRUE(recorder.waitForLogsContaining({"Async callback message"}, ASYNC_LOG_TIMEOUT))
<< "Timed out waiting for log containing 'Async callback message'\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
EXPECT_GT(recorder.getRecordedLogCount(), 0);
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_INFO, "Async callback message"));
}
// Test: Update callback level
TEST_F(IntegrationFrontendUserLogging, UpdateCallbackLevel)
{
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
// Register callback at INFO
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::ASYNC);
auto error = setGlobalLogLevel(HIPDNN_SEV_INFO);
ASSERT_EQ(error.code, ErrorCode::OK);
const auto countBefore = recorder.getRecordedLogCount();
HIPDNN_FE_LOG_INFO("Message at INFO level");
// Wait for the INFO log to arrive
EXPECT_TRUE(recorder.waitForLogCount(countBefore + 1, ASYNC_LOG_TIMEOUT))
<< "Timed out waiting for INFO log delivery\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
const size_t infoLogs = recorder.getRecordedLogCount();
EXPECT_GT(infoLogs, 0);
// Update callback to WARN level (same callback, same handle, different level)
registerUserCallback(HIPDNN_SEV_WARN, LogCallbackMode::ASYNC);
HIPDNN_FE_LOG_INFO("Info after update - should be filtered");
HIPDNN_FE_LOG_WARN("Warn after update - should pass");
// Wait for the WARN log to arrive (INFO should be filtered)
EXPECT_TRUE(recorder.waitForLogsContaining({"Warn after update"}, ASYNC_LOG_TIMEOUT))
<< "Timed out waiting for log containing 'Warn after update'\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
// INFO should be filtered after updating callback to WARN level
EXPECT_FALSE(recorder.hasLogContaining("Info after update"));
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_WARN, "Warn after update"));
}
// Test: Switch between sync and async mode
TEST_F(IntegrationFrontendUserLogging, SwitchBetweenSyncAndAsync)
{
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
// Start with SYNC mode
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::SYNC);
auto error = setGlobalLogLevel(HIPDNN_SEV_INFO);
ASSERT_EQ(error.code, ErrorCode::OK);
HIPDNN_FE_LOG_INFO("Sync mode message");
// Sync: logs immediately available
const size_t syncLogs = recorder.getRecordedLogCount();
EXPECT_GT(syncLogs, 0);
// Switch to ASYNC mode (same callback, same handle, different mode)
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::ASYNC);
HIPDNN_FE_LOG_INFO("Async mode message");
// Wait for async callback to deliver the log
EXPECT_TRUE(recorder.waitForLogsContaining({"Async mode message"}, ASYNC_LOG_TIMEOUT))
<< "Timed out waiting for log containing 'Async mode message'\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
const size_t asyncLogs = recorder.getRecordedLogCount();
EXPECT_GT(asyncLogs, syncLogs);
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_INFO, "Async mode message"));
}
// Test: Switch from async to sync mode
TEST_F(IntegrationFrontendUserLogging, SwitchBetweenAsyncAndSync)
{
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_INFO);
// Start with ASYNC mode
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::ASYNC);
auto error = setGlobalLogLevel(HIPDNN_SEV_INFO);
ASSERT_EQ(error.code, ErrorCode::OK);
const auto countBefore = recorder.getRecordedLogCount();
HIPDNN_FE_LOG_INFO("Async mode message");
// Wait for async callback to deliver the log
EXPECT_TRUE(recorder.waitForLogCount(countBefore + 1, ASYNC_LOG_TIMEOUT))
<< "Timed out waiting for async log delivery\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
const size_t asyncLogs = recorder.getRecordedLogCount();
EXPECT_GT(asyncLogs, 0);
// Switch to SYNC mode (same callback, same handle, different mode)
registerUserCallback(HIPDNN_SEV_INFO, LogCallbackMode::SYNC);
HIPDNN_FE_LOG_INFO("Sync mode message");
// Sync: logs immediately available
const size_t afterSwitch = recorder.getRecordedLogCount();
EXPECT_GT(afterSwitch, asyncLogs);
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_INFO, "Sync mode message"));
}
// Test: Null callback parameter is rejected
TEST_F(IntegrationFrontendUserLogging, RejectsNullCallback)
{
auto error = setUserLogCallback(nullptr, HIPDNN_SEV_INFO, LogCallbackMode::ASYNC, this);
EXPECT_NE(error.code, ErrorCode::OK);
}
// Test: Null userHandle parameter is rejected
TEST_F(IntegrationFrontendUserLogging, RejectsNullHandle)
{
auto error = setUserLogCallback(IsolatedLogRecorder::getIsolatedUserRecordingCallback(),
HIPDNN_SEV_INFO,
LogCallbackMode::ASYNC,
nullptr);
EXPECT_NE(error.code, ErrorCode::OK);
}
// Test: Synchronous guarantee on unregister
TEST_F(IntegrationFrontendUserLogging, SyncGuaranteeOnUnregister)
{
// Track callback invocations
static std::atomic<int> s_callbackCount{0};
static std::mutex s_callbackMutex;
static std::condition_variable s_callbackCV;
auto trackingCallback = [](hipdnnUserLogCallbackHandle_t, hipdnnSeverity_t, const char*) {
s_callbackCount.fetch_add(1);
s_callbackCV.notify_all();
// Simulate some work
std::this_thread::sleep_for(std::chrono::milliseconds(10));
};
int handleToken = 0;
s_callbackCount.store(0);
// Register async callback
auto error = setUserLogCallback(
trackingCallback, HIPDNN_SEV_INFO, LogCallbackMode::ASYNC, &handleToken);
ASSERT_EQ(error.code, ErrorCode::OK);
error = setGlobalLogLevel(HIPDNN_SEV_INFO);
ASSERT_EQ(error.code, ErrorCode::OK);
// Generate many log messages
for(int i = 0; i < 50; ++i)
{
HIPDNN_FE_LOG_INFO("Test message");
}
// Unregister with SEV_OFF - should provide synchronous guarantee
error = setUserLogCallback(
trackingCallback, HIPDNN_SEV_OFF, LogCallbackMode::ASYNC, &handleToken);
ASSERT_EQ(error.code, ErrorCode::OK);
// Take snapshot AFTER unregister to avoid race with async queue processing
const int countAfterUnregister = s_callbackCount.load();
// Generate more logs - callback should NOT receive them
for(int i = 0; i < 10; ++i)
{
HIPDNN_FE_LOG_INFO("After unregister");
}
// Negative assertion: bounded wait to confirm no new callbacks arrive
{
std::unique_lock<std::mutex> lock(s_callbackMutex);
s_callbackCV.wait_for(lock, NEGATIVE_ASSERT_TIMEOUT, [&] {
return s_callbackCount.load() > countAfterUnregister;
});
}
// Count should not have increased after unregister
EXPECT_EQ(s_callbackCount.load(), countAfterUnregister)
<< "No new callbacks should execute after unregister";
}
// Test: Multiple callbacks (2 async + 2 sync) all receive logs independently
TEST_F(IntegrationFrontendUserLogging, MultipleCallbacksAllReceiveLogs)
{
struct CounterWithNotify
{
std::atomic<int> count{0};
std::mutex mtx;
std::condition_variable cv;
};
auto countingCallback
= [](hipdnnUserLogCallbackHandle_t userHandle, hipdnnSeverity_t, const char*) {
auto* c = static_cast<CounterWithNotify*>(userHandle);
c->count.fetch_add(1);
c->cv.notify_all();
};
CounterWithNotify asyncCount1;
CounterWithNotify asyncCount2;
CounterWithNotify syncCount1;
CounterWithNotify syncCount2;
auto error = setGlobalLogLevel(HIPDNN_SEV_INFO);
ASSERT_EQ(error.code, ErrorCode::OK);
// Register 4 callbacks: 2 async, 2 sync
error = setUserLogCallback(
countingCallback, HIPDNN_SEV_INFO, LogCallbackMode::ASYNC, &asyncCount1);
ASSERT_EQ(error.code, ErrorCode::OK);
error = setUserLogCallback(
countingCallback, HIPDNN_SEV_INFO, LogCallbackMode::ASYNC, &asyncCount2);
ASSERT_EQ(error.code, ErrorCode::OK);
error
= setUserLogCallback(countingCallback, HIPDNN_SEV_INFO, LogCallbackMode::SYNC, &syncCount1);
ASSERT_EQ(error.code, ErrorCode::OK);
error
= setUserLogCallback(countingCallback, HIPDNN_SEV_INFO, LogCallbackMode::SYNC, &syncCount2);
ASSERT_EQ(error.code, ErrorCode::OK);
// Trigger logging
HIPDNN_FE_LOG_INFO("Test message for multiple callbacks");
// Wait for async callbacks to deliver logs
{
std::unique_lock<std::mutex> lock(asyncCount1.mtx);
asyncCount1.cv.wait_for(
lock, ASYNC_LOG_TIMEOUT, [&] { return asyncCount1.count.load() > 0; });
}
{
std::unique_lock<std::mutex> lock(asyncCount2.mtx);
asyncCount2.cv.wait_for(
lock, ASYNC_LOG_TIMEOUT, [&] { return asyncCount2.count.load() > 0; });
}
// All 4 callbacks should have received logs
EXPECT_GT(asyncCount1.count.load(), 0) << "First async callback should receive logs";
EXPECT_GT(asyncCount2.count.load(), 0) << "Second async callback should receive logs";
EXPECT_GT(syncCount1.count.load(), 0) << "First sync callback should receive logs";
EXPECT_GT(syncCount2.count.load(), 0) << "Second sync callback should receive logs";
// Unregister all 4
setUserLogCallback(countingCallback, HIPDNN_SEV_OFF, LogCallbackMode::ASYNC, &asyncCount1);
setUserLogCallback(countingCallback, HIPDNN_SEV_OFF, LogCallbackMode::ASYNC, &asyncCount2);
setUserLogCallback(countingCallback, HIPDNN_SEV_OFF, LogCallbackMode::SYNC, &syncCount1);
setUserLogCallback(countingCallback, HIPDNN_SEV_OFF, LogCallbackMode::SYNC, &syncCount2);
// Capture counts after unregistering
const int async1After = asyncCount1.count.load();
const int async2After = asyncCount2.count.load();
const int sync1After = syncCount1.count.load();
const int sync2After = syncCount2.count.load();
// Trigger more logging — none of the callbacks should be invoked
HIPDNN_FE_LOG_INFO("Message after all callbacks unregistered");
// Negative assertion: bounded wait to confirm no new callbacks arrive
{
std::unique_lock<std::mutex> lock(asyncCount1.mtx);
asyncCount1.cv.wait_for(
lock, NEGATIVE_ASSERT_TIMEOUT, [&] { return asyncCount1.count.load() > async1After; });
}
EXPECT_EQ(asyncCount1.count.load(), async1After)
<< "First async callback should not receive logs after unregister";
EXPECT_EQ(asyncCount2.count.load(), async2After)
<< "Second async callback should not receive logs after unregister";
EXPECT_EQ(syncCount1.count.load(), sync1After)
<< "First sync callback should not receive logs after unregister";
EXPECT_EQ(syncCount2.count.load(), sync2After)
<< "Second sync callback should not receive logs after unregister";
}
// Deserializing a graph+plan blob via the no-handle overload drops the plan and
// logs a WARN. This fixture's SetUp only manages the global log level, so the
// test stands up its own handle/plugin environment to build a graph+plan first.
TEST_F(IntegrationFrontendUserLogging, GraphPlusPlanWithoutHandleDropsPlanAndWarns)
{
SKIP_IF_NO_DEVICES();
ASSERT_EQ(hipInit(0), hipSuccess);
const std::string& pluginPath = hipdnn_tests::plugin_constants::testGoodPluginPath();
const std::array<const char*, 1> paths = {pluginPath.c_str()};
ASSERT_EQ(
hipdnnSetEnginePluginPaths_ext(paths.size(), paths.data(), HIPDNN_PLUGIN_LOADING_ABSOLUTE),
HIPDNN_STATUS_SUCCESS);
hipdnnHandle_t handle = nullptr;
ASSERT_EQ(hipdnnCreate(&handle), HIPDNN_STATUS_SUCCESS);
// Finalize a plan so the serialized blob carries one.
auto originalGraph = hipdnn_tests::buildConvFpropGraph();
auto result = originalGraph->build(handle);
ASSERT_EQ(result.code, ErrorCode::OK) << result.err_msg;
auto [data, serErr] = originalGraph->to_binary();
ASSERT_TRUE(serErr.is_good()) << serErr.get_message();
// Install a SYNC WARN recorder so the warning is observable immediately.
auto recorder = IsolatedLogRecorder::withOverrideLevel(HIPDNN_SEV_WARN);
registerUserCallback(HIPDNN_SEV_WARN, LogCallbackMode::SYNC);
auto logLevelError = setGlobalLogLevel(HIPDNN_SEV_WARN);
ASSERT_EQ(logLevelError.code, ErrorCode::OK);
// No-handle overload: the plan must be dropped and a WARN emitted.
auto lifted = std::make_shared<hipdnn_tests::TestableGraphLifting>();
result = lifted->deserialize(data);
EXPECT_TRUE(result.is_good()) << result.err_msg;
EXPECT_TRUE(recorder.hasLogContaining(HIPDNN_SEV_WARN, "no handle was provided"))
<< "Expected a WARN about the dropped execution plan\nRecorded logs:\n"
<< recorder.getRecordedLogsAsString();
EXPECT_FALSE(lifted->hasExecutionPlan())
<< "Execution plan should be dropped when deserializing without a handle";
ASSERT_EQ(hipdnnDestroy(handle), HIPDNN_STATUS_SUCCESS);
}