Files
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

761 lines
31 KiB
C++

// Copyright © Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <gtest/gtest.h>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <optional>
#include <variant>
#include <vector>
#include <nlohmann/json.hpp>
#include <hipdnn_test_sdk/utilities/FileUtilities.hpp>
#include <hipdnn_test_sdk/utilities/LoadGraphAndTensors.hpp>
#include "harness/bundle/BundleDiscovery.hpp"
#include "harness/bundle/IntegrationTestBundle.hpp"
using namespace hipdnn_integration_tests::bundle;
// NOLINTBEGIN(readability-identifier-naming)
namespace
{
class TestBundleDiscoveryFixture : public ::testing::Test
{
protected:
struct SweepCaseSpec
{
std::string id;
std::string ioDataType;
std::vector<int64_t> xDims;
std::vector<int64_t> xStrides;
std::vector<int64_t> derivedDims;
std::vector<int64_t> derivedStrides;
bool includeGolden = true;
bool goldenHasPath = true;
bool includeMetadata = true;
};
std::optional<hipdnn_test_sdk::utilities::ScopedDirectory> _scopedDir;
std::filesystem::path _tempDir;
void SetUp() override
{
auto path
= std::filesystem::temp_directory_path()
/ ("bundle_discovery_test_"
+ std::to_string(::testing::UnitTest::GetInstance()->current_test_info()->line()));
std::filesystem::remove_all(path);
_scopedDir.emplace(path);
_tempDir = _scopedDir->path();
}
static void createMinimalBundle(const std::filesystem::path& dir, const std::string& name)
{
std::filesystem::create_directories(dir);
std::ofstream ofs(dir / (name + ".json"));
ofs << R"({"nodes": [{"inputs": {"x_tensor_uid": 0, "mean_tensor_uid": 1, )"
R"("inv_variance_tensor_uid": 2, "scale_tensor_uid": 3, "bias_tensor_uid": 4}, )"
R"("outputs": {"y_tensor_uid": 5}, "type": "BatchnormInferenceAttributes", )"
R"("compute_data_type": "float", "name": ""}], "tensors": [)"
R"({"name": "", "uid": 0, "strides": [60, 20, 5, 1], "dims": [2, 3, 4, 5], )"
R"("data_type": "float", "virtual": false}, )"
R"({"name": "", "uid": 1, "strides": [3, 1, 1, 1], "dims": [1, 3, 1, 1], )"
R"("data_type": "float", "virtual": false}, )"
R"({"name": "", "uid": 2, "strides": [3, 1, 1, 1], "dims": [1, 3, 1, 1], )"
R"("data_type": "float", "virtual": false}, )"
R"({"name": "", "uid": 3, "strides": [3, 1, 1, 1], "dims": [1, 3, 1, 1], )"
R"("data_type": "float", "virtual": false}, )"
R"({"name": "", "uid": 4, "strides": [3, 1, 1, 1], "dims": [1, 3, 1, 1], )"
R"("data_type": "float", "virtual": false}, )"
R"({"name": "", "uid": 5, "strides": [60, 20, 5, 1], "dims": [2, 3, 4, 5], )"
R"("data_type": "float", "virtual": false}], "io_data_type": "float", )"
R"("compute_data_type": "float", "intermediate_data_type": "float", "name": ""})";
}
// Writes a valid {name}.meta.json companion. Metadata is mandatory for a
// golden bundle (one shipping output .bin blobs) and optional for graph-only
// bundle cases.
static void writeMetadata(const std::filesystem::path& dir, const std::string& name)
{
std::ofstream(dir / (name + ".meta.json"))
<< R"({"format_version": 1, "operation": "BatchnormInference"})";
}
static void createLoadableBundle(const std::filesystem::path& dir, const std::string& name)
{
createMinimalBundle(dir, name);
writeMetadata(dir, name);
const auto basePath = dir / name;
auto writeBin = [&](int64_t uid, size_t byteCount) {
std::vector<char> data(byteCount, 0);
std::ofstream out(basePath.string() + ".tensor" + std::to_string(uid) + ".bin",
std::ios::binary);
out.write(data.data(), static_cast<std::streamsize>(data.size()));
};
writeBin(0, 480);
writeBin(1, 12);
writeBin(2, 12);
writeBin(3, 12);
writeBin(4, 12);
writeBin(5, 480);
}
static size_t elementSizeBytes(const std::string& dataType)
{
if(dataType == "float")
{
return sizeof(float);
}
if(dataType == "half" || dataType == "bfloat16")
{
return 2;
}
throw std::runtime_error("Unsupported test data type: " + dataType);
}
static void writeSweepTensorFile(const std::filesystem::path& dir,
int64_t uid,
const std::vector<int64_t>& dims,
const std::string& dataType)
{
size_t elementCount = 1;
for(const auto dim : dims)
{
elementCount *= static_cast<size_t>(dim);
}
std::vector<char> data(elementCount * elementSizeBytes(dataType), 0);
std::ofstream out(dir / ("tensor" + std::to_string(uid) + ".bin"), std::ios::binary);
out.write(data.data(), static_cast<std::streamsize>(data.size()));
}
static nlohmann::json makeTemplateTensor(int64_t uid)
{
return nlohmann::json{{"name", ""},
{"uid", uid},
{"strides", "${case.strides}"},
{"dims", "${case.dims}"},
{"data_type", "${case.data_type}"},
{"virtual", false}};
}
static nlohmann::json makeSweepTensor(int64_t uid,
const std::string& dataType,
const std::vector<int64_t>& dims,
const std::vector<int64_t>& strides)
{
return nlohmann::json{
{"uid", uid}, {"data_type", dataType}, {"dims", dims}, {"strides", strides}};
}
static void createTemplateSweep(const std::filesystem::path& dir,
const std::vector<SweepCaseSpec>& cases)
{
std::filesystem::create_directories(dir);
const nlohmann::json templateJson
= {{"nodes",
nlohmann::json::array({{{"inputs",
{{"x_tensor_uid", 0},
{"mean_tensor_uid", 1},
{"inv_variance_tensor_uid", 2},
{"scale_tensor_uid", 3},
{"bias_tensor_uid", 4}}},
{"outputs", {{"y_tensor_uid", 5}}},
{"type", "BatchnormInferenceAttributes"},
{"compute_data_type", "float"},
{"name", ""}}})},
{"tensors",
nlohmann::json::array({makeTemplateTensor(0),
makeTemplateTensor(1),
makeTemplateTensor(2),
makeTemplateTensor(3),
makeTemplateTensor(4),
makeTemplateTensor(5)})},
{"io_data_type", "${case.io_data_type}"},
{"compute_data_type", "float"},
{"intermediate_data_type", "float"},
{"name", ""}};
std::ofstream(dir / "graph.template.json") << templateJson.dump(2);
nlohmann::json sweepJson = {{"version", 1}, {"cases", nlohmann::json::array()}};
for(const auto& spec : cases)
{
nlohmann::json caseJson
= {{"id", spec.id},
{"values",
{{"io_data_type", spec.ioDataType},
{"tensors",
nlohmann::json::array(
{makeSweepTensor(0, spec.ioDataType, spec.xDims, spec.xStrides),
makeSweepTensor(1, "float", spec.derivedDims, spec.derivedStrides),
makeSweepTensor(2, "float", spec.derivedDims, spec.derivedStrides),
makeSweepTensor(3, "float", spec.derivedDims, spec.derivedStrides),
makeSweepTensor(4, "float", spec.derivedDims, spec.derivedStrides),
makeSweepTensor(5, spec.ioDataType, spec.xDims, spec.xStrides)})}}}};
if(spec.includeGolden)
{
caseJson["golden"]
= spec.goldenHasPath
? nlohmann::json{{"path", "golden/" + spec.id + "/tensors.dvc"}}
: nlohmann::json::object();
if(spec.goldenHasPath)
{
const auto goldenDir = dir / "golden" / spec.id;
std::filesystem::create_directories(goldenDir);
std::ofstream(goldenDir / "tensors.dvc") << "outs:\n";
writeSweepTensorFile(goldenDir, 0, spec.xDims, spec.ioDataType);
writeSweepTensorFile(goldenDir, 1, spec.derivedDims, "float");
writeSweepTensorFile(goldenDir, 2, spec.derivedDims, "float");
writeSweepTensorFile(goldenDir, 3, spec.derivedDims, "float");
writeSweepTensorFile(goldenDir, 4, spec.derivedDims, "float");
writeSweepTensorFile(goldenDir, 5, spec.xDims, spec.ioDataType);
}
}
if(spec.includeMetadata)
{
caseJson["metadata"] = nlohmann::json{
{"format_version", 1}, {"operation", "BatchnormInference"}, {"seed", 42}};
}
sweepJson["cases"].push_back(std::move(caseJson));
}
std::ofstream(dir / "sweep.json") << sweepJson.dump(2);
}
static const DiscoveredBundle* findByTest(const std::vector<DiscoveredBundle>& bundles,
const std::string& testName)
{
for(const auto& b : bundles)
{
if(b.testName == testName)
{
return &b;
}
}
return nullptr;
}
};
} // namespace
TEST_F(TestBundleDiscoveryFixture, FlatCustomerBundleDrop)
{
createMinimalBundle(_tempDir / "case_23421", "graph");
auto result = discoverBundles(_tempDir);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result.front().suiteName, "case_23421");
EXPECT_EQ(result.front().testName, "graph");
}
TEST_F(TestBundleDiscoveryFixture, TieredGoldenDataLayoutIsDiscovered)
{
createMinimalBundle(_tempDir / "quick" / "BatchnormFwdInference" / "ncdhw" / "fp32" / "Small",
"Small");
auto result = discoverBundles(_tempDir);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result.front().suiteName, "quick_BatchnormFwdInference_ncdhw_fp32_Small");
EXPECT_EQ(result.front().testName, "Small");
}
TEST_F(TestBundleDiscoveryFixture, TemplateSweepCasesAreExpandedFromManifest)
{
createTemplateSweep(
_tempDir / "quick" / "BatchnormFwdInference" / "Inference",
{{"small_fp32_nchw", "float", {2, 3, 4, 5}, {60, 20, 5, 1}, {1, 3, 1, 1}, {3, 1, 1, 1}},
{"small_fp16_nchw", "half", {2, 3, 4, 5}, {60, 20, 5, 1}, {1, 3, 1, 1}, {3, 1, 1, 1}}});
auto result = discoverBundles(_tempDir);
ASSERT_EQ(result.size(), 2u);
const auto* fp32 = findByTest(result, "small_fp32_nchw");
ASSERT_NE(fp32, nullptr);
EXPECT_TRUE(fp32->isTemplateSweepCase());
EXPECT_EQ(fp32->suiteName, "quick_BatchnormFwdInference_Inference");
EXPECT_EQ(fp32->jsonPath,
_tempDir / "quick" / "BatchnormFwdInference" / "Inference" / "sweep.json");
EXPECT_EQ(fp32->sweep->templatePath,
_tempDir / "quick" / "BatchnormFwdInference" / "Inference" / "graph.template.json");
EXPECT_EQ(fp32->sweep->caseId, "small_fp32_nchw");
const auto* fp16 = findByTest(result, "small_fp16_nchw");
ASSERT_NE(fp16, nullptr);
EXPECT_TRUE(fp16->isTemplateSweepCase());
EXPECT_EQ(fp16->suiteName, "quick_BatchnormFwdInference_Inference");
EXPECT_EQ(fp16->sweep->caseId, "small_fp16_nchw");
}
TEST_F(TestBundleDiscoveryFixture, JsonAtRootUsesFolderNameAsSuite)
{
// A .json directly at the data root uses the root folder name as suite.
std::ofstream(_tempDir / "graph.json") << R"({"tensors": []})";
auto result = discoverBundles(_tempDir);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result[0].suiteName, sanitizeForGtest(_tempDir.filename().string()));
EXPECT_EQ(result[0].testName, "graph");
}
TEST_F(TestBundleDiscoveryFixture, EmptyLeafFolderWarnsAndSkips)
{
createMinimalBundle(_tempDir / "conv" / "good", "good");
std::filesystem::create_directories(_tempDir / "conv" / "case_12312");
auto result = discoverBundles(_tempDir);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result.front().testName, "good");
}
TEST_F(TestBundleDiscoveryFixture, LeafWithOnlyMetaJsonWarnsAndSkips)
{
auto dir = _tempDir / "conv" / "meta_only";
std::filesystem::create_directories(dir);
std::ofstream(dir / "meta.json") << "{}";
auto result = discoverBundles(_tempDir);
EXPECT_TRUE(result.empty());
}
TEST_F(TestBundleDiscoveryFixture, EmptyRootReturnsEmpty)
{
auto result = discoverBundles(_tempDir);
EXPECT_TRUE(result.empty());
}
TEST_F(TestBundleDiscoveryFixture, CollisionThrows)
{
createMinimalBundle(_tempDir / "Op-A" / "case", "SameName");
createMinimalBundle(_tempDir / "Op_A" / "case", "SameName");
EXPECT_THROW(discoverBundles(_tempDir), std::runtime_error);
}
TEST_F(TestBundleDiscoveryFixture, CustomerDropAndTieredLayoutCoexistUnderOneRoot)
{
createMinimalBundle(_tempDir / "case_1", "graph");
createMinimalBundle(_tempDir / "conv" / "nchw" / "fp16" / "resnet50", "resnet50");
auto result = discoverBundles(_tempDir);
ASSERT_EQ(result.size(), 2u);
const auto* flat = findByTest(result, "graph");
ASSERT_NE(flat, nullptr);
EXPECT_EQ(flat->suiteName, "case_1");
const auto* deep = findByTest(result, "resnet50");
ASSERT_NE(deep, nullptr);
EXPECT_EQ(deep->suiteName, "conv_nchw_fp16_resnet50");
}
TEST_F(TestBundleDiscoveryFixture, SkipsMetaJson)
{
auto bundleDir = _tempDir / "conv" / "nchw" / "fp32" / "withmeta";
createMinimalBundle(bundleDir, "withmeta");
std::ofstream(bundleDir / "withmeta.meta.json") << "{}";
std::ofstream(bundleDir / "meta.json") << "{}";
auto result = discoverBundles(_tempDir);
ASSERT_EQ(result.size(), 1u);
EXPECT_EQ(result.front().testName, "withmeta");
}
TEST_F(TestBundleDiscoveryFixture, ScanFilesByExtensionIsGenericAndSorted)
{
std::filesystem::create_directories(_tempDir / "b");
std::filesystem::create_directories(_tempDir / "a");
std::ofstream(_tempDir / "b" / "z.json") << "{}";
std::ofstream(_tempDir / "a" / "m.json") << "{}";
std::ofstream(_tempDir / "a" / "ignore.txt") << "x";
auto files = scanFilesByExtension(_tempDir, ".json");
ASSERT_EQ(files.size(), 2u);
EXPECT_EQ(files[0].filename(), "m.json");
EXPECT_EQ(files[1].filename(), "z.json");
}
TEST(TestGraphFile, AllowlistsGraphsAndExcludesCompanions)
{
EXPECT_TRUE(isGraphFile("dir/resnet50.json"));
EXPECT_TRUE(isGraphFile("Small.json"));
EXPECT_FALSE(isGraphFile("dir/resnet50.meta.json"));
EXPECT_FALSE(isGraphFile("dir/meta.json"));
EXPECT_FALSE(isGraphFile("dir/graph.template.json"));
EXPECT_FALSE(isGraphFile("dir/sweep.json"));
EXPECT_TRUE(isGraphFile("dir/model.fp16.json"));
EXPECT_TRUE(isGraphFile("dir/resnet50.v2.json"));
EXPECT_TRUE(isGraphFile("dir/resnet50.claims.json"));
EXPECT_FALSE(isGraphFile("dir/resnet50.bin"));
EXPECT_FALSE(isGraphFile("dir/resnet50.tensor0.bin"));
}
TEST(TestSanitizeForGtest, ReplacesInvalidChars)
{
EXPECT_EQ(sanitizeForGtest("resnet50-layer3.v2"), "resnet50_layer3_v2");
EXPECT_EQ(sanitizeForGtest("name with spaces"), "name_with_spaces");
EXPECT_EQ(sanitizeForGtest("already_ok"), "already_ok");
}
TEST_F(TestBundleDiscoveryFixture, UnparseableJsonIsDiscoveredButLoadThrows)
{
auto badDir = _tempDir / "BadOp" / "Malformed";
std::filesystem::create_directories(badDir);
std::ofstream(badDir / "Malformed.json") << "{{NOT VALID JSON AT ALL";
auto bundles = discoverBundles(_tempDir);
auto it = std::find_if(bundles.begin(), bundles.end(), [](const DiscoveredBundle& b) {
return b.testName == "Malformed";
});
ASSERT_NE(it, bundles.end()) << "Malformed bundle should be discovered (valid .json path)";
EXPECT_THROW(hipdnn_test_sdk::utilities::loadGraphAndTensors(it->jsonPath), std::exception);
}
TEST_F(TestBundleDiscoveryFixture, LoadBundlePopulatesAllFields)
{
auto dir = _tempDir / "op" / "loadtest";
createLoadableBundle(dir, "loadtest");
const auto jsonPath = dir / "loadtest.json";
auto result = loadIntegrationTestBundle(jsonPath);
ASSERT_TRUE(std::holds_alternative<IntegrationTestBundle>(result));
auto& bundle = std::get<IntegrationTestBundle>(result);
ASSERT_EQ(bundle.outputTensorUids.size(), 1u);
EXPECT_EQ(bundle.outputTensorUids.front(), 5);
ASSERT_TRUE(bundle.tensors.has_value());
EXPECT_EQ(bundle.tensors->size(), 6u);
EXPECT_NE(bundle.tensors->find(5), bundle.tensors->end());
ASSERT_TRUE(bundle.metadata.operation.has_value());
EXPECT_EQ(*bundle.metadata.operation, "BatchnormInference");
}
TEST_F(TestBundleDiscoveryFixture, LoadBundlePopulatesMetadataWhenPresent)
{
auto dir = _tempDir / "op" / "withmeta";
createMinimalBundle(dir, "withmeta");
std::ofstream(dir / "withmeta.meta.json")
<< R"({"format_version": 1, "operation": "BatchnormInference", "seed": 42})";
const auto jsonPath = dir / "withmeta.json";
auto result = loadIntegrationTestBundle(jsonPath);
ASSERT_TRUE(std::holds_alternative<IntegrationTestBundle>(result));
auto& bundle = std::get<IntegrationTestBundle>(result);
ASSERT_TRUE(bundle.metadata.operation.has_value());
EXPECT_EQ(*bundle.metadata.operation, "BatchnormInference");
ASSERT_TRUE(bundle.metadata.seed.has_value());
EXPECT_EQ(*bundle.metadata.seed, 42);
}
// A graph-only bundle (no .bin blobs, hence no golden data) without a .meta.json
// companion loads successfully: metadata validates golden data, and there is
// none here, so absent metadata is valid and default-constructed.
TEST_F(TestBundleDiscoveryFixture, LoadGraphOnlyBundleMissingMetadataLoads)
{
auto dir = _tempDir / "op" / "nometa";
createMinimalBundle(dir, "nometa"); // graph only, no .meta.json, no .bin
const auto jsonPath = dir / "nometa.json";
auto result = loadIntegrationTestBundle(jsonPath);
ASSERT_TRUE(std::holds_alternative<IntegrationTestBundle>(result));
const auto& bundle = std::get<IntegrationTestBundle>(result);
EXPECT_FALSE(bundle.tensors.has_value()); // graph-only: no tensor data
EXPECT_FALSE(bundle.hasGoldenOutputs);
EXPECT_FALSE(bundle.metadata.operation.has_value()); // default-constructed
}
// A GOLDEN bundle (output .bin blobs present) WITHOUT a .meta.json companion is
// a load error: metadata is mandatory whenever there is golden data to validate.
TEST_F(TestBundleDiscoveryFixture, LoadGoldenBundleMissingMetadataIsError)
{
auto dir = _tempDir / "op" / "goldennometa";
createLoadableBundle(dir, "goldennometa"); // writes .bin (inputs+outputs) + meta
std::filesystem::remove(dir / "goldennometa.meta.json"); // drop the metadata
const auto jsonPath = dir / "goldennometa.json";
auto result = loadIntegrationTestBundle(jsonPath);
ASSERT_TRUE(std::holds_alternative<LoadError>(result));
EXPECT_EQ(std::get<LoadError>(result), LoadError::MISSING_METADATA);
}
TEST_F(TestBundleDiscoveryFixture, LoadBundleMissingBinIsGraphOnly)
{
auto dir = _tempDir / "op" / "nobin";
createMinimalBundle(dir, "nobin");
writeMetadata(dir, "nobin"); // metadata present (optional here, but exercised)
const auto jsonPath = dir / "nobin.json";
auto result = loadIntegrationTestBundle(jsonPath);
ASSERT_TRUE(std::holds_alternative<IntegrationTestBundle>(result));
const auto& bundle = std::get<IntegrationTestBundle>(result);
EXPECT_FALSE(bundle.tensors.has_value());
EXPECT_EQ(bundle.outputTensorUids.size(), 1u);
}
TEST_F(TestBundleDiscoveryFixture, LoadTemplateSweepCasePopulatesExpandedGraphAndTensorData)
{
createTemplateSweep(
_tempDir / "quick" / "BatchnormFwdInference" / "Inference",
{{"small_fp16_nchw", "half", {2, 3, 4, 5}, {60, 20, 5, 1}, {1, 3, 1, 1}, {3, 1, 1, 1}}});
const auto discovered = discoverBundles(_tempDir);
ASSERT_EQ(discovered.size(), 1u);
ASSERT_TRUE(discovered.front().isTemplateSweepCase());
auto result = loadIntegrationTestBundle(discovered.front());
ASSERT_TRUE(std::holds_alternative<IntegrationTestBundle>(result));
const auto& bundle = std::get<IntegrationTestBundle>(result);
ASSERT_TRUE(bundle.tensors.has_value());
ASSERT_EQ(bundle.outputTensorUids.size(), 1u);
EXPECT_EQ(bundle.outputTensorUids.front(), 5);
EXPECT_EQ(bundle.tensors->at(0)->dims(), (std::vector<int64_t>{2, 3, 4, 5}));
EXPECT_EQ(bundle.tensors->at(0)->strides(), (std::vector<int64_t>{60, 20, 5, 1}));
EXPECT_EQ(bundle.tensors->at(5)->dims(), (std::vector<int64_t>{2, 3, 4, 5}));
const auto tensorAttrMap = bundle.graphWrapper().getTensorMap();
EXPECT_EQ(tensorAttrMap.at(0)->data_type(),
hipdnn_flatbuffers_sdk::data_objects::DataType::HALF);
EXPECT_EQ(tensorAttrMap.at(1)->data_type(),
hipdnn_flatbuffers_sdk::data_objects::DataType::FLOAT);
ASSERT_TRUE(bundle.metadata.operation.has_value());
EXPECT_EQ(*bundle.metadata.operation, "BatchnormInference");
ASSERT_TRUE(bundle.metadata.seed.has_value());
EXPECT_EQ(*bundle.metadata.seed, 42);
}
TEST_F(TestBundleDiscoveryFixture, LoadTemplateSweepCaseWithoutGoldenIsGraphOnly)
{
createTemplateSweep(_tempDir / "quick" / "BatchnormFwdInference" / "Inference",
{{"graph_only_fp32_nchw",
"float",
{2, 3, 4, 5},
{60, 20, 5, 1},
{1, 3, 1, 1},
{3, 1, 1, 1},
false}});
const auto discovered = discoverBundles(_tempDir);
ASSERT_EQ(discovered.size(), 1u);
auto result = loadIntegrationTestBundle(discovered.front());
ASSERT_TRUE(std::holds_alternative<IntegrationTestBundle>(result));
const auto& bundle = std::get<IntegrationTestBundle>(result);
EXPECT_FALSE(bundle.tensors.has_value());
}
TEST_F(TestBundleDiscoveryFixture, LoadTemplateSweepCaseMissingGoldenPathIsError)
{
createTemplateSweep(_tempDir / "quick" / "BatchnormFwdInference" / "Inference",
{{"missing_path_fp32_nchw",
"float",
{2, 3, 4, 5},
{60, 20, 5, 1},
{1, 3, 1, 1},
{3, 1, 1, 1},
true,
false}});
const auto discovered = discoverBundles(_tempDir);
ASSERT_EQ(discovered.size(), 1u);
auto result = loadIntegrationTestBundle(discovered.front());
ASSERT_TRUE(std::holds_alternative<LoadError>(result));
EXPECT_EQ(std::get<LoadError>(result), LoadError::INVALID_SWEEP_CASE);
}
TEST_F(TestBundleDiscoveryFixture, LoadTemplateSweepCaseMissingTensorValueIsError)
{
const auto sweepDir = _tempDir / "quick" / "BatchnormFwdInference" / "Inference";
createTemplateSweep(sweepDir,
{{"missing_tensor_value_fp32_nchw",
"float",
{2, 3, 4, 5},
{60, 20, 5, 1},
{1, 3, 1, 1},
{3, 1, 1, 1}}});
auto sweepJson = nlohmann::json::parse(std::ifstream(sweepDir / "sweep.json"));
sweepJson["cases"][0]["values"]["tensors"].erase(
sweepJson["cases"][0]["values"]["tensors"].begin());
std::ofstream(sweepDir / "sweep.json") << sweepJson.dump(2);
const auto discovered = discoverBundles(_tempDir);
ASSERT_EQ(discovered.size(), 1u);
auto result = loadIntegrationTestBundle(discovered.front());
ASSERT_TRUE(std::holds_alternative<LoadError>(result));
EXPECT_EQ(std::get<LoadError>(result), LoadError::INVALID_SWEEP_CASE);
}
TEST_F(TestBundleDiscoveryFixture, LoadBundleWrongSizeBinIsTensorLoadError)
{
auto dir = _tempDir / "op" / "badbin";
createLoadableBundle(dir, "badbin");
std::ofstream(dir / "badbin.tensor0.bin", std::ios::binary) << "too short";
const auto jsonPath = dir / "badbin.json";
auto result = loadIntegrationTestBundle(jsonPath);
ASSERT_TRUE(std::holds_alternative<LoadError>(result));
EXPECT_EQ(std::get<LoadError>(result), LoadError::TENSOR_LOAD_FAILED);
}
TEST_F(TestBundleDiscoveryFixture, LoadBundleMissingTensorsKeyIsSchemaError)
{
auto dir = _tempDir / "op" / "notensorskey";
std::filesystem::create_directories(dir);
std::ofstream(dir / "notensorskey.json") << R"({"nodes": []})";
auto result = loadIntegrationTestBundle(dir / "notensorskey.json");
ASSERT_TRUE(std::holds_alternative<LoadError>(result));
EXPECT_EQ(std::get<LoadError>(result), LoadError::INVALID_GRAPH_SCHEMA);
}
TEST_F(TestBundleDiscoveryFixture, LoadBundleMalformedJsonIsError)
{
auto dir = _tempDir / "op" / "badjson";
std::filesystem::create_directories(dir);
std::ofstream(dir / "badjson.json") << "{{NOT VALID";
auto result = loadIntegrationTestBundle(dir / "badjson.json");
ASSERT_TRUE(std::holds_alternative<LoadError>(result));
EXPECT_EQ(std::get<LoadError>(result), LoadError::MALFORMED_JSON);
}
TEST_F(TestBundleDiscoveryFixture, LoadBundleMissingFileIsError)
{
auto result = loadIntegrationTestBundle(_tempDir / "does_not_exist.json");
ASSERT_TRUE(std::holds_alternative<LoadError>(result));
EXPECT_EQ(std::get<LoadError>(result), LoadError::MALFORMED_JSON);
}
// A golden sweep case that omits its inline `metadata` block is rejected with
// MISSING_METADATA: metadata is what validates the golden data.
TEST_F(TestBundleDiscoveryFixture, LoadTemplateSweepCaseGoldenWithoutMetadataIsError)
{
createTemplateSweep(_tempDir / "quick" / "BatchnormFwdInference" / "Inference",
{{"golden_no_meta_fp32_nchw",
"float",
{2, 3, 4, 5},
{60, 20, 5, 1},
{1, 3, 1, 1},
{3, 1, 1, 1},
true, // includeGolden
true, // goldenHasPath
false}}); // includeMetadata
const auto discovered = discoverBundles(_tempDir);
ASSERT_EQ(discovered.size(), 1u);
auto result = loadIntegrationTestBundle(discovered.front());
ASSERT_TRUE(std::holds_alternative<LoadError>(result));
EXPECT_EQ(std::get<LoadError>(result), LoadError::MISSING_METADATA);
}
// Every sweep case must carry metadata, golden or not: a graph-only case that
// omits its metadata block is also MISSING_METADATA.
TEST_F(TestBundleDiscoveryFixture, LoadTemplateSweepCaseWithoutMetadataIsError)
{
createTemplateSweep(_tempDir / "quick" / "BatchnormFwdInference" / "Inference",
{{"graph_only_no_meta_fp32_nchw",
"float",
{2, 3, 4, 5},
{60, 20, 5, 1},
{1, 3, 1, 1},
{3, 1, 1, 1},
false, // includeGolden
false, // goldenHasPath
false}}); // includeMetadata
const auto discovered = discoverBundles(_tempDir);
ASSERT_EQ(discovered.size(), 1u);
auto result = loadIntegrationTestBundle(discovered.front());
ASSERT_TRUE(std::holds_alternative<LoadError>(result));
EXPECT_EQ(std::get<LoadError>(result), LoadError::MISSING_METADATA);
}
// A sweep root whose manifest has two cases sharing the same id is rejected
// wholesale: readSweepCaseIds throws on the duplicate and discoverBundles lets
// it propagate, so a broken checked-in manifest fails the run instead of
// silently dropping sibling cases.
TEST_F(TestBundleDiscoveryFixture, DuplicateSweepCaseIdThrows)
{
createTemplateSweep(
_tempDir / "quick" / "BatchnormFwdInference" / "Inference",
{{"dup_case_nchw", "float", {2, 3, 4, 5}, {60, 20, 5, 1}, {1, 3, 1, 1}, {3, 1, 1, 1}},
{"dup_case_nchw", "half", {2, 3, 4, 5}, {60, 20, 5, 1}, {1, 3, 1, 1}, {3, 1, 1, 1}}});
EXPECT_THROW(discoverBundles(_tempDir), std::runtime_error);
}
// resolvePlaceholder falls back to the case-level `values` map when a
// placeholder key is absent from the per-tensor entry. A top-level template
// placeholder (no current tensor uid) resolves straight from the global value.
TEST_F(TestBundleDiscoveryFixture, ExpandTemplateResolvesPlaceholderFromGlobalCaseValue)
{
const nlohmann::json templateJson
= {{"io_data_type", "${case.io_data_type}"},
{"tensors", nlohmann::json::array({{{"uid", 0}, {"data_type", "float"}}})}};
// io_data_type is supplied only at the top level of `values`, not inside any
// values.tensors[] entry.
const nlohmann::json caseJson = {
{"id", "global_fallback"},
{"values", {{"io_data_type", "half"}, {"tensors", nlohmann::json::array({{{"uid", 0}}})}}}};
DiscoveredBundle discovered;
discovered.jsonPath = _tempDir / "sweep.json";
const auto expanded = detail::expandTemplateGraph(templateJson, caseJson, discovered);
EXPECT_EQ(expanded.at("io_data_type").get<std::string>(), "half");
}
// An unused top-level sweep value triggers a warning (warnUnusedSweepValues),
// not a load error. Assert the bundle still loads to keep the warning path
// distinct from the INVALID_SWEEP_CASE error path.
TEST_F(TestBundleDiscoveryFixture, UnusedSweepValueWarnsButLoadSucceeds)
{
const auto sweepDir = _tempDir / "quick" / "BatchnormFwdInference" / "Inference";
createTemplateSweep(sweepDir,
{{"unused_value_fp32_nchw",
"float",
{2, 3, 4, 5},
{60, 20, 5, 1},
{1, 3, 1, 1},
{3, 1, 1, 1}}});
// Inject a top-level case value that no ${case.*} placeholder consumes.
auto sweepJson = nlohmann::json::parse(std::ifstream(sweepDir / "sweep.json"));
sweepJson["cases"][0]["values"]["extra_key"] = 99;
std::ofstream(sweepDir / "sweep.json") << sweepJson.dump(2);
const auto discovered = discoverBundles(_tempDir);
ASSERT_EQ(discovered.size(), 1u);
auto result = loadIntegrationTestBundle(discovered.front());
ASSERT_TRUE(std::holds_alternative<IntegrationTestBundle>(result));
}
// NOLINTEND(readability-identifier-naming)