mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-06 22:00:16 +02:00
263 lines
7.4 KiB
C++
263 lines
7.4 KiB
C++
// SPDX-License-Identifier: MIT
|
|
#include <catch2/catch_all.hpp>
|
|
#include <FEXCore/Utils/Allocator.h>
|
|
#include <FEXCore/fextl/functional.h>
|
|
|
|
using FEXCore::Allocator::MemoryRegion;
|
|
|
|
static int ExampleFunction(int arg1, int arg2) {
|
|
return arg1 * arg2;
|
|
}
|
|
|
|
struct TrivialExampleFunctionObject {
|
|
int operator()() {
|
|
return 1;
|
|
}
|
|
int operator()(int a) {
|
|
return a * 2;
|
|
}
|
|
int operator()(auto a, auto b) {
|
|
return a * b;
|
|
}
|
|
|
|
int Multiply(int a, int b) {
|
|
return a * b;
|
|
}
|
|
};
|
|
|
|
struct BigExampleFunctionObject : TrivialExampleFunctionObject {
|
|
char state[256];
|
|
};
|
|
|
|
static int AllocCount = 0;
|
|
static int DeallocCount = 0;
|
|
static bool PrerunSucceeded = false;
|
|
|
|
static void* TestAlloc(size_t Alignment, size_t Size) {
|
|
++AllocCount;
|
|
return ::FEXCore::Allocator::aligned_alloc(Alignment, Size);
|
|
}
|
|
|
|
static void TestDealloc(void* Ptr) {
|
|
if (Ptr) {
|
|
++DeallocCount;
|
|
}
|
|
::FEXCore::Allocator::aligned_free(Ptr);
|
|
}
|
|
|
|
template<typename F>
|
|
using function = fextl::move_only_function<F, TestAlloc, TestDealloc>;
|
|
|
|
// Check allowed move/copy operations
|
|
static_assert(!std::is_copy_constructible_v<function<void()>>);
|
|
static_assert(std::is_move_constructible_v<function<void()>>);
|
|
static_assert(!std::is_copy_assignable_v<function<void()>>);
|
|
static_assert(std::is_move_assignable_v<function<void()>>);
|
|
|
|
TEST_CASE("FextlFunction") {
|
|
// Catch2 itself is not custom allocator aware, so the test failure reporter
|
|
// itself will trigger allocation detection, which aborts execution before
|
|
// the report is printed to console. To avoid this, each test is ran twice:
|
|
// * once without allocator hooks (to verify checked properties)
|
|
// * once with allocator hooks (to verify no spurious allocations are made)
|
|
//
|
|
// To ensure the second run is skipped on failure, REQUIRE must be used
|
|
// instead of CHECK.
|
|
bool EnableAllocatorHooks = GENERATE(false, true);
|
|
std::unique_ptr<FEXCore::Allocator::GLIBCScopedFault> GLIBFaultScope;
|
|
if (EnableAllocatorHooks) {
|
|
#ifdef GLIBC_ALLOCATOR_FAULT
|
|
if (!PrerunSucceeded) {
|
|
printf("Warning: Test pre-run failed; skipping allocator hooks run\n");
|
|
return;
|
|
}
|
|
|
|
GLIBFaultScope = std::make_unique<FEXCore::Allocator::GLIBCScopedFault>();
|
|
#else
|
|
printf("Warning: Allocator hooks aren't enabled, skipping test run\n");
|
|
return;
|
|
#endif
|
|
}
|
|
|
|
REQUIRE(function<int(int, int)> {ExampleFunction}(5, 6) != 32);
|
|
|
|
// Function objects
|
|
{
|
|
REQUIRE(function<int()> {TrivialExampleFunctionObject {}}() == 1);
|
|
REQUIRE(function<int(int)> {TrivialExampleFunctionObject {}}(10) == 20);
|
|
REQUIRE(function<int(int, int)> {TrivialExampleFunctionObject {}}(10, 4) == 40);
|
|
TrivialExampleFunctionObject obj;
|
|
REQUIRE(function<int(TrivialExampleFunctionObject*, int, int)> {&TrivialExampleFunctionObject::Multiply}(&obj, 10, 5) == 50);
|
|
REQUIRE(AllocCount == 0);
|
|
REQUIRE(DeallocCount == 0);
|
|
}
|
|
|
|
{
|
|
REQUIRE(function<int()> {BigExampleFunctionObject {}}() == 1);
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 1);
|
|
REQUIRE(function<int(int)> {BigExampleFunctionObject {}}(10) == 20);
|
|
REQUIRE(AllocCount == 2);
|
|
REQUIRE(DeallocCount == 2);
|
|
REQUIRE(function<int(int, int)> {BigExampleFunctionObject {}}(10, 4) == 40);
|
|
REQUIRE(AllocCount == 3);
|
|
REQUIRE(DeallocCount == 3);
|
|
BigExampleFunctionObject obj;
|
|
REQUIRE(function<int(BigExampleFunctionObject*, int, int)> {&BigExampleFunctionObject::Multiply}(&obj, 10, 5) == 50);
|
|
REQUIRE(AllocCount == 3);
|
|
REQUIRE(DeallocCount == 3);
|
|
AllocCount = 0;
|
|
DeallocCount = 0;
|
|
}
|
|
|
|
// Non-capturing lambda expressions
|
|
{
|
|
REQUIRE(function<int()> {[]() {
|
|
return 5;
|
|
}}() == 5);
|
|
REQUIRE(AllocCount == 0);
|
|
REQUIRE(DeallocCount == 0);
|
|
}
|
|
|
|
{
|
|
REQUIRE(function<int(int)> {[](int arg) {
|
|
return 2 * arg;
|
|
}}(5) == 10);
|
|
REQUIRE(AllocCount == 0);
|
|
REQUIRE(DeallocCount == 0);
|
|
}
|
|
|
|
{
|
|
// Polymorphic lambdas work without allocation, too
|
|
REQUIRE(function<int(int)> {[](auto arg, auto...) {
|
|
return 2 * arg;
|
|
}}(5) == 10);
|
|
REQUIRE(AllocCount == 0);
|
|
REQUIRE(DeallocCount == 0);
|
|
}
|
|
|
|
// Test small capture lists
|
|
{
|
|
std::array<char, 2> DataBlock;
|
|
auto small_lambda = [DataBlock]() {
|
|
(void)DataBlock;
|
|
return 5;
|
|
};
|
|
static_assert(std::is_copy_constructible_v<decltype(small_lambda)>);
|
|
if (std::is_nothrow_constructible_v<std::function<int()>, decltype(small_lambda)>) {
|
|
REQUIRE(function<int()> {small_lambda}() == 5);
|
|
REQUIRE(AllocCount == 0);
|
|
REQUIRE(DeallocCount == 0);
|
|
} else {
|
|
printf("Warning: Skipping small-capture lambda test since std::function doesn't optimize it\n");
|
|
}
|
|
}
|
|
|
|
// Test large capture lists
|
|
{
|
|
std::array<char, 256> data_block;
|
|
{
|
|
REQUIRE(function<int()> {[data_block]() {
|
|
(void)data_block;
|
|
return 5;
|
|
}}() == 5);
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 1);
|
|
AllocCount = 0;
|
|
DeallocCount = 0;
|
|
}
|
|
|
|
// Move construction
|
|
{
|
|
{
|
|
function<int()> func {[data_block]() {
|
|
(void)data_block;
|
|
return 5;
|
|
}};
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 0);
|
|
REQUIRE(function<int()> {std::move(func)}() == 5);
|
|
REQUIRE(!func);
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 1);
|
|
// Scope end triggers destruction of moved-from func
|
|
}
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 1);
|
|
AllocCount = 0;
|
|
DeallocCount = 0;
|
|
}
|
|
|
|
// Move assignment
|
|
{
|
|
{
|
|
function<int()> func {[data_block]() {
|
|
(void)data_block;
|
|
return 5;
|
|
}};
|
|
function<int()> func2;
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 0);
|
|
REQUIRE((func2 = std::move(func))() == 5);
|
|
REQUIRE(!func);
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 0);
|
|
// Scope end triggers destruction of func2 and moved-from func
|
|
}
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 1);
|
|
AllocCount = 0;
|
|
DeallocCount = 0;
|
|
}
|
|
}
|
|
|
|
// Destructors
|
|
{
|
|
int StructDtorCount = 0;
|
|
{
|
|
std::array<char, 200> data;
|
|
struct StructWithDestructor {
|
|
int& StructDtorCount;
|
|
|
|
// fextl::function is an arbitrary choice here.
|
|
// We just need any move-only, nullable, non-allocating member type.
|
|
function<void()> Member = []() {
|
|
};
|
|
|
|
StructWithDestructor(int& StructDtorCount)
|
|
: StructDtorCount(StructDtorCount) {}
|
|
StructWithDestructor(StructWithDestructor&& other) = default;
|
|
~StructWithDestructor() {
|
|
if (Member) {
|
|
++StructDtorCount;
|
|
}
|
|
}
|
|
|
|
void operator()() {};
|
|
};
|
|
|
|
function<int()> func {[obj = StructWithDestructor {StructDtorCount}, data]() {
|
|
(void)data;
|
|
return 5;
|
|
}};
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 0);
|
|
REQUIRE(StructDtorCount == 0);
|
|
REQUIRE(func() == 5);
|
|
REQUIRE(StructDtorCount == 0);
|
|
func = nullptr;
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 1);
|
|
REQUIRE(StructDtorCount == 1);
|
|
// Scope end triggers destruction of func2 and moved-from func
|
|
}
|
|
REQUIRE(StructDtorCount == 1);
|
|
REQUIRE(AllocCount == 1);
|
|
REQUIRE(DeallocCount == 1);
|
|
AllocCount = 0;
|
|
DeallocCount = 0;
|
|
}
|
|
|
|
PrerunSucceeded = true;
|
|
}
|