Files
TheRock/rocm-libraries/shared/stinkytofu/tests/unit/ErrorHandlingTest.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

488 lines
16 KiB
C++

/* ************************************************************************
* Copyright (C) 2025 Advanced Micro Devices, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* ************************************************************************ */
// Unit tests for error handling mechanisms in StinkyTofu
// Tests Expected<T> and STINKY_UNREACHABLE() functionality
#include <gtest/gtest.h>
#include <chrono>
#include <memory>
#include <string>
#include <vector>
#include "stinkytofu/support/ErrorHandling.hpp"
using namespace stinkytofu;
// ============================================================================
// Expected<T> - Basic Tests
// ============================================================================
class ExpectedTest : public ::testing::Test {
protected:
void SetUp() override {}
};
// Test successful Expected creation with primitive types
TEST_F(ExpectedTest, SuccessWithInt) {
Expected<int> result(42);
EXPECT_TRUE(result.hasValue());
EXPECT_FALSE(result.hasError());
EXPECT_TRUE(static_cast<bool>(result));
EXPECT_EQ(*result, 42);
}
// Test successful Expected creation with pointer types
TEST_F(ExpectedTest, SuccessWithPointer) {
int value = 123;
Expected<int*> result(&value);
EXPECT_TRUE(result.hasValue());
EXPECT_FALSE(result.hasError());
EXPECT_EQ(*result, &value);
EXPECT_EQ(**result, 123);
}
// Test successful Expected creation with std::string
TEST_F(ExpectedTest, SuccessWithString) {
Expected<std::string> result("hello world");
EXPECT_TRUE(result.hasValue());
EXPECT_FALSE(result.hasError());
EXPECT_EQ(*result, "hello world");
}
// Test successful Expected creation with std::vector
TEST_F(ExpectedTest, SuccessWithVector) {
std::vector<int> vec = {1, 2, 3, 4, 5};
Expected<std::vector<int>> result(vec);
EXPECT_TRUE(result.hasValue());
EXPECT_FALSE(result.hasError());
EXPECT_EQ(result->size(), 5);
EXPECT_EQ((*result)[2], 3);
}
// Test error Expected creation
TEST_F(ExpectedTest, ErrorCreation) {
Expected<int> result = Expected<int>::Error("Something went wrong");
EXPECT_FALSE(result.hasValue());
EXPECT_TRUE(result.hasError());
EXPECT_FALSE(static_cast<bool>(result));
EXPECT_EQ(result.getError(), "Something went wrong");
}
// Test error Expected with detailed message
TEST_F(ExpectedTest, ErrorWithDetailedMessage) {
std::string archName = "gfx900";
Expected<void*> result = Expected<void*>::Error("Instruction s_mul_lo_u32 not supported on " +
archName + " (requires gfx1250+)");
EXPECT_TRUE(result.hasError());
EXPECT_TRUE(result.getError().find("gfx900") != std::string::npos);
EXPECT_TRUE(result.getError().find("gfx1250") != std::string::npos);
}
// ============================================================================
// Expected<T> - Move Semantics Tests
// ============================================================================
TEST_F(ExpectedTest, MoveConstructorSuccess) {
Expected<std::string> original("test string");
Expected<std::string> moved(std::move(original));
EXPECT_TRUE(moved.hasValue());
EXPECT_EQ(*moved, "test string");
}
TEST_F(ExpectedTest, MoveConstructorError) {
Expected<int> original = Expected<int>::Error("error message");
Expected<int> moved(std::move(original));
EXPECT_TRUE(moved.hasError());
EXPECT_EQ(moved.getError(), "error message");
}
TEST_F(ExpectedTest, MoveAssignmentSuccess) {
Expected<int> original(999);
Expected<int> assigned(0);
assigned = std::move(original);
EXPECT_TRUE(assigned.hasValue());
EXPECT_EQ(*assigned, 999);
}
TEST_F(ExpectedTest, MoveAssignmentError) {
Expected<int> original = Expected<int>::Error("test error");
Expected<int> assigned(0);
assigned = std::move(original);
EXPECT_TRUE(assigned.hasError());
EXPECT_EQ(assigned.getError(), "test error");
}
// ============================================================================
// Expected<T> - Access Operators Tests
// ============================================================================
TEST_F(ExpectedTest, DereferenceOperator) {
Expected<int> result(42);
EXPECT_EQ(*result, 42);
// Test mutability through dereference
*result = 100;
EXPECT_EQ(*result, 100);
}
TEST_F(ExpectedTest, ArrowOperator) {
struct TestStruct {
int value;
std::string name;
};
TestStruct obj{42, "test"};
Expected<TestStruct> result(obj);
EXPECT_EQ(result->value, 42);
EXPECT_EQ(result->name, "test");
// Test mutability through arrow operator
result->value = 100;
EXPECT_EQ(result->value, 100);
}
TEST_F(ExpectedTest, RValueDereference) {
auto makeExpected = []() -> Expected<std::string> { return std::string("temporary"); };
std::string value = *makeExpected();
EXPECT_EQ(value, "temporary");
}
// ============================================================================
// Expected<T> - Pattern Matching / Error Handling Tests
// ============================================================================
// Helper function that returns Expected<int>
static Expected<int> divideOrError(int numerator, int denominator) {
if (denominator == 0) {
return Expected<int>::Error("Division by zero");
}
return numerator / denominator;
}
TEST_F(ExpectedTest, PatternMatchingSuccess) {
auto result = divideOrError(10, 2);
if (result) {
EXPECT_EQ(*result, 5);
} else {
FAIL() << "Expected success but got error: " << result.getError();
}
}
TEST_F(ExpectedTest, PatternMatchingError) {
auto result = divideOrError(10, 0);
if (result) {
FAIL() << "Expected error but got success: " << *result;
} else {
EXPECT_EQ(result.getError(), "Division by zero");
}
}
// ============================================================================
// Expected<T> - Complex Type Tests
// ============================================================================
struct ComplexType {
std::vector<int> data;
std::unique_ptr<int> ptr;
std::string name;
ComplexType(std::vector<int> d, int val, std::string n)
: data(std::move(d)), ptr(std::make_unique<int>(val)), name(std::move(n)) {}
// Move-only type
ComplexType(ComplexType&&) noexcept = default;
ComplexType& operator=(ComplexType&&) noexcept = default;
ComplexType(const ComplexType&) = delete;
ComplexType& operator=(const ComplexType&) = delete;
};
TEST_F(ExpectedTest, MoveOnlyType) {
ComplexType obj({1, 2, 3}, 42, "test");
Expected<ComplexType> result(std::move(obj));
EXPECT_TRUE(result.hasValue());
EXPECT_EQ(result->data.size(), 3);
EXPECT_EQ(*result->ptr, 42);
EXPECT_EQ(result->name, "test");
}
TEST_F(ExpectedTest, UniquePointer) {
auto ptr = std::make_unique<int>(999);
Expected<std::unique_ptr<int>> result(std::move(ptr));
EXPECT_TRUE(result.hasValue());
EXPECT_EQ(**result, 999);
}
// ============================================================================
// Expected<T> - Real-World Use Cases
// ============================================================================
// Simulate architecture check function
static Expected<void*> createInstructionForArch(const std::string& arch,
const std::string& instruction) {
if (arch == "gfx900" && instruction == "s_mul_lo_u32") {
return Expected<void*>::Error("s_mul_lo_u32 not supported on gfx900 (requires gfx1250+)");
}
if (arch == "gfx1250") {
return static_cast<void*>(nullptr); // Placeholder
}
return Expected<void*>::Error("Unknown architecture: " + arch);
}
TEST_F(ExpectedTest, ArchitectureLimitation) {
auto result1 = createInstructionForArch("gfx900", "s_mul_lo_u32");
EXPECT_TRUE(result1.hasError());
EXPECT_TRUE(result1.getError().find("gfx1250") != std::string::npos);
auto result2 = createInstructionForArch("gfx1250", "s_mul_lo_u32");
EXPECT_TRUE(result2.hasValue());
}
TEST_F(ExpectedTest, ChainedErrorPropagation) {
// Simulate function that depends on another Expected-returning function
auto processInstruction = [](const std::string& arch) -> Expected<int> {
auto inst = createInstructionForArch(arch, "s_mul_lo_u32");
if (!inst) {
return Expected<int>::Error("Failed to create instruction: " + inst.getError());
}
return 42; // Success
};
auto result1 = processInstruction("gfx900");
EXPECT_TRUE(result1.hasError());
EXPECT_TRUE(result1.getError().find("Failed to create instruction") != std::string::npos);
auto result2 = processInstruction("gfx1250");
EXPECT_TRUE(result2.hasValue());
EXPECT_EQ(*result2, 42);
}
// ============================================================================
// Expected<T> - Edge Cases
// ============================================================================
TEST_F(ExpectedTest, EmptyString) {
Expected<std::string> result("");
EXPECT_TRUE(result.hasValue());
EXPECT_EQ(result->size(), 0);
}
TEST_F(ExpectedTest, EmptyErrorMessage) {
Expected<int> result = Expected<int>::Error("");
EXPECT_TRUE(result.hasError());
EXPECT_EQ(result.getError(), "");
}
TEST_F(ExpectedTest, LargeErrorMessage) {
std::string longMsg(10000, 'x');
Expected<int> result = Expected<int>::Error(longMsg);
EXPECT_TRUE(result.hasError());
EXPECT_EQ(result.getError().size(), 10000);
}
TEST_F(ExpectedTest, NullPointer) {
Expected<int*> result(nullptr);
EXPECT_TRUE(result.hasValue());
EXPECT_EQ(*result, nullptr);
}
// ============================================================================
// STINKY_UNREACHABLE() Tests
// ============================================================================
// Note: STINKY_UNREACHABLE() aborts the process, so we can't directly test it
// in unit tests. Instead, we test that the code compiles and that we can
// use it in dead code paths.
class UnreachableTest : public ::testing::Test {
protected:
void SetUp() override {}
};
// Test that STINKY_UNREACHABLE compiles and can be used in conditional branches
TEST_F(UnreachableTest, CompilationTest) {
auto testFunction = [](int value) -> int {
if (value == 0) {
return 0;
} else if (value == 1) {
return 1;
} else {
// This branch should never execute in this test
if (value == 2) {
return 2;
}
// If we somehow get here with value != 2, it's a bug
// STINKY_UNREACHABLE("Unexpected value");
// Note: Can't actually execute this in test, just verify compilation
return -1; // Placeholder for test
}
};
EXPECT_EQ(testFunction(0), 0);
EXPECT_EQ(testFunction(1), 1);
EXPECT_EQ(testFunction(2), 2);
}
// Test pattern: validation followed by unreachable
TEST_F(UnreachableTest, ValidationPattern) {
auto createWithValidation = [](bool isValid) -> Expected<int> {
if (!isValid) {
return Expected<int>::Error("Validation failed");
}
// After validation, some operation that should never fail
int* ptr = new int(42);
if (!ptr) {
// This should never happen on modern systems
// STINKY_UNREACHABLE("Failed to allocate memory after validation");
// Can't execute in test, but pattern is correct
}
int value = *ptr;
delete ptr;
return value;
};
auto result1 = createWithValidation(true);
EXPECT_TRUE(result1.hasValue());
auto result2 = createWithValidation(false);
EXPECT_TRUE(result2.hasError());
}
// ============================================================================
// Integration Tests - Expected + assert pattern
// ============================================================================
TEST_F(ExpectedTest, AssertPlusExpectedPattern) {
// Simulate function with precondition checks and recoverable errors
auto processData = [](int* data, size_t size, bool supportedArch) -> Expected<int> {
// Debug-time precondition check
assert(data != nullptr && "Data pointer must not be null");
assert(size > 0 && "Size must be positive");
// Recoverable error (architecture limitation)
if (!supportedArch) {
return Expected<int>::Error("Operation not supported on this architecture");
}
// Success
int sum = 0;
for (size_t i = 0; i < size; i++) {
sum += data[i];
}
return sum;
};
int testData[] = {1, 2, 3, 4, 5};
auto result1 = processData(testData, 5, true);
EXPECT_TRUE(result1.hasValue());
EXPECT_EQ(*result1, 15);
auto result2 = processData(testData, 5, false);
EXPECT_TRUE(result2.hasError());
EXPECT_TRUE(result2.getError().find("not supported") != std::string::npos);
}
// ============================================================================
// Performance Tests (Sanity Check)
// ============================================================================
TEST_F(ExpectedTest, PerformanceSanity) {
// Test that Expected doesn't have crazy overhead for simple types
const int iterations = 10000;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; i++) {
Expected<int> result(i);
if (result) {
volatile int x = *result; // Prevent optimization
(void)x;
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
// This should complete in well under 1ms for 10k iterations
EXPECT_LT(duration, 10000) << "Expected<T> has unreasonable overhead";
}
// ============================================================================
// Documentation Examples Tests
// ============================================================================
// Verify examples from ErrorHandling.hpp actually work
TEST_F(ExpectedTest, DocumentationExample1) {
auto createInstruction = [](bool arch_supports) -> Expected<int*> {
if (!arch_supports) {
return Expected<int*>::Error("Not supported");
}
static int inst = 42;
return &inst;
};
// Usage from documentation
auto result = createInstruction(false);
if (!result) {
EXPECT_EQ(result.getError(), "Not supported");
} else {
FAIL() << "Should have failed";
}
}
TEST_F(ExpectedTest, DocumentationExample2) {
struct StinkyRegister {
bool isValid() const {
return true;
}
};
auto createInst = [](StinkyRegister dst) -> Expected<int*> {
assert(dst.isValid() && "Invalid destination register");
static int inst = 42;
return &inst;
};
StinkyRegister reg;
auto result = createInst(reg);
EXPECT_TRUE(result.hasValue());
}