Files
TheRock/rocm-libraries/projects/hipdnn/backend/tests/TestHeuristicPluginManagerValidationPaths.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

536 lines
21 KiB
C++

// Copyright © Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
/**
* @file TestHeuristicPluginManagerValidationPaths.cpp
* @brief Tests for HeuristicPluginManager validation code paths
*
* These tests load actual test plugins to exercise validateBeforeAdding() and
* actionAfterAdding() to improve coverage of HeuristicPluginManager.hpp
*/
#include "HipdnnException.hpp"
#include "PlatformUtils.hpp"
#include "TestPluginConstants.hpp"
#include "plugin/HeuristicPluginManager.hpp"
#include <filesystem>
#include <gtest/gtest.h>
#include <hipdnn_data_sdk/utilities/PlatformUtils.hpp>
#include <hipdnn_plugin_sdk/heuristic_api_version.h>
#include <hipdnn_test_sdk/utilities/FileUtilities.hpp>
// Test plugin name constants (defined here because CMake ordering prevents proper macro propagation)
namespace
{
constexpr const char* BAD_API_VERSION_PLUGIN = "test_bad_api_version_heuristic_plugin";
constexpr const char* EMPTY_NAME_PLUGIN = "test_empty_name_heuristic_plugin";
constexpr const char* DUPLICATE_POLICY_ID_A_PLUGIN = "test_duplicate_policy_id_a_plugin";
constexpr const char* DUPLICATE_POLICY_ID_B_PLUGIN = "test_duplicate_policy_id_b_plugin";
} // namespace
using namespace hipdnn_backend;
using namespace hipdnn_backend::plugin;
using namespace hipdnn_backend::plugin_constants;
class TestHeuristicPluginManagerValidationPaths : public ::testing::Test
{
protected:
static void SetUpTestSuite()
{
// Check once if test plugins are available
const auto pluginPath = getHeuristicPluginPath("").parent_path();
if(!std::filesystem::exists(pluginPath))
{
GTEST_SKIP() << "Test plugins not found at: " << pluginPath
<< "\nMake sure test_plugins are built before running tests";
}
}
void SetUp() override
{
// Test plugins are in lib/test_plugins/custom relative to backend library location
_testPluginPath = getHeuristicPluginPath("").parent_path();
// Create manager for each test
_manager = std::make_unique<HeuristicPluginManager>();
}
std::unique_ptr<HeuristicPluginManager> _manager;
std::filesystem::path _testPluginPath;
};
// ========== validateBeforeAdding() Tests - API Version Check ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, GoodPluginPassesApiVersionValidation)
{
// Load good test plugin - should pass API version validation
EXPECT_NO_THROW(_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE));
const auto& plugins = _manager->getPlugins();
// Should have loaded at least the good plugin
EXPECT_GT(plugins.size(), 0);
// All loaded plugins should have correct API major version
for(const auto& plugin : plugins)
{
const auto version = hipdnn_data_sdk::utilities::Version{plugin->apiVersion()};
EXPECT_EQ(version.major, HIPDNN_HEURISTIC_API_VERSION_MAJOR)
<< "validateBeforeAdding should have checked API version for plugin: "
<< plugin->name();
}
}
// ========== validateBeforeAdding() Tests - Policy ID Uniqueness ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, ActionAfterAddingStoresPolicyIds)
{
// Load plugins - actionAfterAdding should store policy IDs
_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const auto& plugins = _manager->getPlugins();
// Collect all policy IDs to verify actionAfterAdding was called
std::set<int64_t> policyIds;
size_t totalPolicyCount = 0;
for(const auto& plugin : plugins)
{
for(const int64_t id : plugin->getAllPolicyIds())
{
++totalPolicyCount;
// Each policy ID should be unique (actionAfterAdding should have tracked this)
EXPECT_EQ(policyIds.count(id), 0) << "Policy ID " << id
<< " appears multiple times (actionAfterAdding "
"tracking failed)";
policyIds.insert(id);
}
}
// Should have loaded plugins with unique policy IDs
EXPECT_GT(policyIds.size(), 0) << "Should have loaded at least one plugin policy";
EXPECT_EQ(policyIds.size(), totalPolicyCount) << "All policy IDs should be unique";
}
TEST_F(TestHeuristicPluginManagerValidationPaths, PolicyIdTrackingAcrossMultiplePlugins)
{
// Load all available plugins
_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const auto& plugins = _manager->getPlugins();
// Collect all policy IDs from every loaded plugin/policy
std::set<int64_t> policyIds;
size_t totalPolicyCount = 0;
for(const auto& plugin : plugins)
{
for(const int64_t id : plugin->getAllPolicyIds())
{
++totalPolicyCount;
// Each policy ID should be unique (actionAfterAdding tracks this)
EXPECT_EQ(policyIds.count(id), 0)
<< "Policy ID " << id << " appears multiple times (validateBeforeAdding failed)";
policyIds.insert(id);
}
}
// Should have as many unique IDs as policies across all plugins
EXPECT_EQ(policyIds.size(), totalPolicyCount);
}
// ========== validateBeforeAdding() Tests - Policy Name Check ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, AllLoadedPluginsHaveNonEmptyNames)
{
_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const auto& plugins = _manager->getPlugins();
// validateBeforeAdding should have rejected any plugins with empty names
for(const auto& plugin : plugins)
{
EXPECT_FALSE(plugin->name().empty())
<< "validateBeforeAdding should reject plugins with empty policy names";
}
}
TEST_F(TestHeuristicPluginManagerValidationPaths, PolicyNameIsProvided)
{
_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const auto& plugins = _manager->getPlugins();
EXPECT_GT(plugins.size(), 0);
for(const auto& plugin : plugins)
{
// Plugin name must be non-empty (validated in HeuristicPluginManager)
const std::string pluginName(plugin->name());
EXPECT_FALSE(pluginName.empty()) << "Plugin has empty name";
// Each policy must have a non-empty name (validated eagerly in HeuristicPlugin)
for(const int64_t policyId : plugin->getAllPolicyIds())
{
const std::string policyName(plugin->getPolicyName(policyId));
EXPECT_FALSE(policyName.empty()) << "Policy ID " << policyId << " has empty name";
}
}
}
// ========== Validation Success Path Tests ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, ValidPluginPassesAllValidation)
{
// Load should succeed for valid plugins
EXPECT_NO_THROW(_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE));
const auto& plugins = _manager->getPlugins();
for(const auto& plugin : plugins)
{
// API version check
const auto version = hipdnn_data_sdk::utilities::Version{plugin->apiVersion()};
EXPECT_EQ(version.major, HIPDNN_HEURISTIC_API_VERSION_MAJOR);
// Plugin name check
EXPECT_FALSE(plugin->name().empty());
// Each policy ID should be non-zero
for(const int64_t policyId : plugin->getAllPolicyIds())
{
EXPECT_NE(policyId, 0);
}
}
}
// ========== actionAfterAdding() Coverage Tests ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, ActionAfterAddingExecutesForEachPlugin)
{
_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const auto& plugins = _manager->getPlugins();
// For each policy across all plugins, actionAfterAdding should have inserted the
// policy ID into _policyIds set. Verified indirectly by ensuring no duplicates exist.
std::set<int64_t> observedIds;
for(const auto& plugin : plugins)
{
for(const int64_t id : plugin->getAllPolicyIds())
{
EXPECT_EQ(observedIds.count(id), 0)
<< "Duplicate policy ID detected - actionAfterAdding may have failed";
observedIds.insert(id);
}
}
}
// ========== Multiple Load Cycles with Validation ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, ValidationRunsOnEachLoadCycle)
{
// Create new manager for each load to ensure fresh state
HeuristicPluginManager manager1; // NOLINT(misc-const-correctness)
manager1.loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const size_t count1 = manager1.getPlugins().size();
// Second manager should also run validation and load same plugins
HeuristicPluginManager manager2; // NOLINT(misc-const-correctness)
manager2.loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const size_t count2 = manager2.getPlugins().size();
EXPECT_EQ(count1, count2) << "Both managers should validate and load same plugins";
EXPECT_GT(count1, 0) << "Should have loaded at least one plugin";
}
TEST_F(TestHeuristicPluginManagerValidationPaths, AbsoluteReloadResetsPolicyIdTracking)
{
// Regression: ABSOLUTE-mode reload must clear the derived-class policy-id
// index, not just _plugins. Otherwise reloading a plugin whose policy id
// matches one from the previous load triggers a false "already exists"
// failure in validateBeforeAdding.
_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const size_t firstCount = _manager->getPlugins().size();
ASSERT_GT(firstCount, 0u) << "Test precondition: at least one plugin must load";
EXPECT_NO_THROW(_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE));
EXPECT_EQ(_manager->getPlugins().size(), firstCount);
}
// ========== Constructor Path Coverage ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, ConstructorSetsUpValidationInfrastructure)
{
// Constructor initializes with search paths and empty _policyIds set.
// Assert against the freshly-constructed local manager (not the fixture's),
// otherwise the test is just re-checking SetUp's invariant.
const HeuristicPluginManager manager;
// A freshly-constructed manager always contains the Config + StaticOrdering
// built-ins (registered in HeuristicPluginManager's constructor); nothing else yet.
EXPECT_EQ(manager.getPlugins().size(), 2u);
}
// ========== Destructor Path Coverage ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, DestructorUnloadsLoadedPluginLibraries)
{
_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
ASSERT_FALSE(_manager->getPlugins().empty())
<< "Test precondition: at least one plugin must load to exercise the unload path";
// Destruction triggers SharedLibrary teardown for each loaded plugin
// (dlclose / FreeLibrary). ASAN catches any leak in plugin-side static teardown.
EXPECT_NO_THROW(_manager.reset());
EXPECT_EQ(_manager, nullptr);
}
// ========== Integration with PluginManagerBase ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, ValidationIntegratesWithBaseClass)
{
// PluginManagerBase calls validateBeforeAdding before adding each plugin
// and actionAfterAdding after successful add
_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const auto& plugins = _manager->getPlugins();
// All plugins should have passed validation
for(const auto& plugin : plugins)
{
// These checks verify that validateBeforeAdding was called and passed
EXPECT_FALSE(plugin->name().empty());
const auto version = hipdnn_data_sdk::utilities::Version{plugin->apiVersion()};
EXPECT_EQ(version.major, HIPDNN_HEURISTIC_API_VERSION_MAJOR);
for(const int64_t policyId : plugin->getAllPolicyIds())
{
EXPECT_NE(policyId, 0);
}
}
}
// ========== Specific Test Plugin Tests ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, NoOptionalHeuristicPluginPassesValidation)
{
// test_no_optional_heuristic_plugin doesn't implement optional functions
// but should still pass validation
_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const auto& plugins = _manager->getPlugins();
ASSERT_FALSE(plugins.empty()) << "Expected at least one plugin to load from "
<< _testPluginPath;
bool foundNoOptional = false;
for(const auto& plugin : plugins)
{
const std::string name(plugin->name());
if(name.find("NoOptional") != std::string::npos)
{
foundNoOptional = true;
// Should pass all validation checks
EXPECT_FALSE(name.empty());
for(const int64_t policyId : plugin->getAllPolicyIds())
{
EXPECT_NE(policyId, 0);
}
}
}
EXPECT_TRUE(foundNoOptional) << "test_no_optional_heuristic_plugin should be loaded";
}
TEST_F(TestHeuristicPluginManagerValidationPaths, GoodHeuristicPluginPassesValidation)
{
_manager->loadPlugins({_testPluginPath}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
const auto& plugins = _manager->getPlugins();
ASSERT_FALSE(plugins.empty()) << "Expected at least one plugin to load from "
<< _testPluginPath;
// Match the exact good-plugin name. A loose substring match would pass even if
// the good plugin failed to load.
constexpr const char* K_GOOD_PLUGIN_NAME = "TestGoodHeuristicPlugin";
bool foundGood = false;
for(const auto& plugin : plugins)
{
const std::string name(plugin->name());
if(name == K_GOOD_PLUGIN_NAME)
{
foundGood = true;
// Verify it passed all validation:
// 1. API version
const auto version = hipdnn_data_sdk::utilities::Version{plugin->apiVersion()};
EXPECT_EQ(version.major, HIPDNN_HEURISTIC_API_VERSION_MAJOR);
// 2. Plugin name is non-empty
EXPECT_FALSE(name.empty());
// 3. All policy IDs are unique and non-zero
for(const int64_t policyId : plugin->getAllPolicyIds())
{
EXPECT_NE(policyId, 0);
}
}
}
EXPECT_TRUE(foundGood) << "test_good_heuristic_plugin should be loaded";
}
// ========== Validation Failure Tests ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, BadApiVersionPluginRejected)
{
// ABSOLUTE mode accepts a single plugin file path, so we can load just the bad
// plugin directly from the build tree instead of copying it to a temp dir.
const auto badPlugin
= _testPluginPath / hipdnn_data_sdk::utilities::getLibraryName(BAD_API_VERSION_PLUGIN);
// Without this precondition, loadPlugins silently no-ops on a missing file
// and the empty-plugins assertion below would pass vacuously.
ASSERT_TRUE(std::filesystem::exists(badPlugin))
<< "Test precondition: bad-API-version plugin missing at " << badPlugin;
_manager->loadPlugins({badPlugin}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
// Built-in Config + StaticOrdering are always present; no external plugin should have loaded.
EXPECT_EQ(_manager->getPlugins().size(), 2u) << "Bad API version plugin should be rejected";
}
TEST_F(TestHeuristicPluginManagerValidationPaths, EmptyNamePluginRejected)
{
const auto emptyNamePlugin
= _testPluginPath / hipdnn_data_sdk::utilities::getLibraryName(EMPTY_NAME_PLUGIN);
ASSERT_TRUE(std::filesystem::exists(emptyNamePlugin))
<< "Test precondition: empty-name plugin missing at " << emptyNamePlugin;
_manager->loadPlugins({emptyNamePlugin}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
// Built-in Config + StaticOrdering are always present; no external plugin should have loaded.
EXPECT_EQ(_manager->getPlugins().size(), 2u) << "Empty policy name plugin should be rejected";
}
TEST_F(TestHeuristicPluginManagerValidationPaths, DuplicatePolicyIdPluginsRejected)
{
const auto pluginA = _testPluginPath
/ hipdnn_data_sdk::utilities::getLibraryName(DUPLICATE_POLICY_ID_A_PLUGIN);
const auto pluginB = _testPluginPath
/ hipdnn_data_sdk::utilities::getLibraryName(DUPLICATE_POLICY_ID_B_PLUGIN);
if(!std::filesystem::exists(pluginA) || !std::filesystem::exists(pluginB))
{
GTEST_SKIP() << "test_duplicate_policy_id plugins not found";
}
_manager->loadPlugins({pluginA, pluginB}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
// Built-in Config + StaticOrdering are always present, plus the first of the
// duplicate pair (pluginA). The second (pluginB) is rejected for duplicate policy ID.
const auto& plugins = _manager->getPlugins();
ASSERT_EQ(plugins.size(), 3u) << "Built-ins + first duplicate plugin should be present";
// The survivor must be pluginA (first offered). Probe pluginA on its own to
// capture its policy IDs, then verify those IDs appear in the loaded set —
// without this, the size check would still pass if pluginA was rejected for
// an unrelated reason and pluginB loaded.
HeuristicPluginManager probeA;
probeA.loadPlugins({pluginA}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
ASSERT_EQ(probeA.getPlugins().size(), 3u)
<< "pluginA should load successfully alongside the built-ins to be a valid baseline";
// Find the non-built-in plugin in the probe to get pluginA's policy IDs.
std::vector<int64_t> pluginAPolicyIds;
for(const auto& plugin : probeA.getPlugins())
{
const std::string name(plugin->name());
if(name != "BuiltInStaticOrderingHeuristic" && name != "BuiltInConfigHeuristic")
{
pluginAPolicyIds = plugin->getAllPolicyIds();
break;
}
}
ASSERT_FALSE(pluginAPolicyIds.empty()) << "Probe failed to identify pluginA";
bool foundPluginA = false;
for(const auto& plugin : plugins)
{
if(plugin->getAllPolicyIds() == pluginAPolicyIds)
{
foundPluginA = true;
break;
}
}
EXPECT_TRUE(foundPluginA) << "Survivor should be pluginA (the first offered), not pluginB";
}
// ========== loadPluginFromFile Return-Value Regression ==========
// Expose loadPluginFromFile() so we can directly observe its bool return.
// The bug it guards against: success was set to true at the top of the
// tryCatch lambda, so a throwing validateBeforeAdding() (e.g. bad API
// version) left success == true and the caller's failedCount silently
// stayed at zero.
class LoadPluginFromFileProbe : public HeuristicPluginManager
{
public:
using HeuristicPluginManager::loadPluginFromFile;
};
TEST_F(TestHeuristicPluginManagerValidationPaths, LoadPluginFromFileReturnsFalseOnValidationFailure)
{
const auto badPlugin
= _testPluginPath / hipdnn_data_sdk::utilities::getLibraryName(BAD_API_VERSION_PLUGIN);
ASSERT_TRUE(std::filesystem::exists(badPlugin))
<< "Test precondition: bad-API-version plugin missing at " << badPlugin;
LoadPluginFromFileProbe probe;
const size_t pluginCountBefore = probe.getPlugins().size();
EXPECT_FALSE(probe.loadPluginFromFile(badPlugin))
<< "loadPluginFromFile must report failure when validateBeforeAdding throws";
EXPECT_EQ(probe.getPlugins().size(), pluginCountBefore)
<< "Rejected plugin must not be appended to _plugins";
}
TEST_F(TestHeuristicPluginManagerValidationPaths, LoadPluginFromFileReturnsTrueOnSuccess)
{
const auto goodPlugin = getHeuristicPluginPath("test_good_heuristic_plugin");
ASSERT_TRUE(std::filesystem::exists(goodPlugin))
<< "Test precondition: good plugin missing at " << goodPlugin;
LoadPluginFromFileProbe probe;
const size_t pluginCountBefore = probe.getPlugins().size();
EXPECT_TRUE(probe.loadPluginFromFile(goodPlugin));
EXPECT_EQ(probe.getPlugins().size(), pluginCountBefore + 1u);
// A second load of the same file is an idempotent no-op (already in
// _loadedPluginFiles); it must also return true so failedCount is
// not inflated by retries.
EXPECT_TRUE(probe.loadPluginFromFile(goodPlugin))
<< "Idempotent reload of an already-loaded plugin must not count as failure";
EXPECT_EQ(probe.getPlugins().size(), pluginCountBefore + 1u);
}
// ========== Edge Case: Empty Plugin Directory ==========
TEST_F(TestHeuristicPluginManagerValidationPaths, EmptyDirectorySkipsValidation)
{
// ScopedDirectory creates the dir and remove_all's it on destruction, so
// the temp dir is cleaned up even if an assertion below aborts.
const auto uniqueName = std::string("hipdnn_empty_heur_test_")
+ std::to_string(::testing::UnitTest::GetInstance()->random_seed());
const hipdnn_test_sdk::utilities::ScopedDirectory emptyDir(
std::filesystem::temp_directory_path() / uniqueName);
// Load from empty directory - no plugins to validate
_manager->loadPlugins({emptyDir.path()}, HIPDNN_PLUGIN_LOADING_ABSOLUTE);
// Built-in Config + StaticOrdering are always present; the empty directory contributed nothing.
EXPECT_EQ(_manager->getPlugins().size(), 2u);
}