Files
ProjectEleri/Plugins/DirectiveUtilities/Source/DirectiveUtilitiesTests/Private/Tests/DirectiveUtilMathFunctionLibraryTest.cpp
2026-07-15 19:17:47 +02:00

246 lines
16 KiB
C++

#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMathFunctionLibraryTest, "DirectiveUtilities.MathFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
{
TestEqual("AngleBetweenVectors should return 0 for parallel vectors",
UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, FVector::ForwardVector), 0.0f);
TestTrue("AngleBetweenVectors should return ~90 for perpendicular vectors",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, FVector::RightVector), 90.0f, 0.01f));
TestTrue("AngleBetweenVectors should return ~180 for opposite vectors",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, -FVector::ForwardVector), 180.0f, 0.01f));
TestTrue("AngleBetweenVectors should handle zero vector gracefully",
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ZeroVector, FVector::ForwardVector)));
{
const FVector2D Sample2D(12.34f, 56.78f);
const float Noise2DFirst = UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(Sample2D);
const float Noise2DSecond = UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(Sample2D);
TestEqual("PerlinNoise2D should be deterministic for the same input", Noise2DFirst, Noise2DSecond);
TestTrue("PerlinNoise2D should return a finite value", FMath::IsFinite(Noise2DFirst));
TestTrue("PerlinNoise2D should be within [-1, 1]",
Noise2DFirst >= -1.0f - 1.e-4f && Noise2DFirst <= 1.0f + 1.e-4f);
}
{
const FVector2D Samples2D[] = {
FVector2D(0.5f, 0.5f),
FVector2D(-3.25f, 7.1f),
FVector2D(100.123f, -200.456f),
FVector2D(0.0f, 0.0f)
};
for (const FVector2D& Sample : Samples2D)
{
const float Value = UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(Sample);
TestTrue(FString::Printf(TEXT("PerlinNoise2D should be finite at %s"), *Sample.ToString()), FMath::IsFinite(Value));
TestTrue(FString::Printf(TEXT("PerlinNoise2D should be within [-1,1] at %s"), *Sample.ToString()),
Value >= -1.0f - 1.e-4f && Value <= 1.0f + 1.e-4f);
}
}
TestTrue("PerlinNoise2D should be ~0 at an integer lattice point",
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(FVector2D(3.0f, 4.0f)), 1.e-4f));
{
const FVector Sample3D(12.34f, 56.78f, 90.12f);
const float Noise3DFirst = UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(Sample3D);
const float Noise3DSecond = UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(Sample3D);
TestEqual("PerlinNoise3D should be deterministic for the same input", Noise3DFirst, Noise3DSecond);
TestTrue("PerlinNoise3D should return a finite value", FMath::IsFinite(Noise3DFirst));
TestTrue("PerlinNoise3D should be within [-1, 1]",
Noise3DFirst >= -1.0f - 1.e-4f && Noise3DFirst <= 1.0f + 1.e-4f);
}
{
const FVector Samples3D[] = {
FVector(0.5f, 0.5f, 0.5f),
FVector(-3.25f, 7.1f, -1.9f),
FVector(100.123f, -200.456f, 33.7f),
FVector(0.0f, 0.0f, 0.0f)
};
for (const FVector& Sample : Samples3D)
{
const float Value = UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(Sample);
TestTrue(FString::Printf(TEXT("PerlinNoise3D should be finite at %s"), *Sample.ToString()), FMath::IsFinite(Value));
TestTrue(FString::Printf(TEXT("PerlinNoise3D should be within [-1,1] at %s"), *Sample.ToString()),
Value >= -1.0f - 1.e-4f && Value <= 1.0f + 1.e-4f);
}
}
TestTrue("PerlinNoise3D should be ~0 at an integer lattice point",
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(FVector(3.0f, 4.0f, 5.0f)), 1.e-4f));
const TArray<EDirectiveUtilEaseType> AllEaseTypes = {
EDirectiveUtilEaseType::BackIn, EDirectiveUtilEaseType::BackOut, EDirectiveUtilEaseType::BackInOut,
EDirectiveUtilEaseType::ElasticIn, EDirectiveUtilEaseType::ElasticOut, EDirectiveUtilEaseType::ElasticInOut,
EDirectiveUtilEaseType::BounceIn, EDirectiveUtilEaseType::BounceOut, EDirectiveUtilEaseType::BounceInOut
};
for (const EDirectiveUtilEaseType EaseType : AllEaseTypes)
{
const FString TypeName = FString::FromInt(static_cast<int32>(EaseType));
TestTrue(FString::Printf(TEXT("EaseAlpha(0) should be ~0 for type %s"), *TypeName),
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EaseType), 0.0f, 1.e-3f));
TestTrue(FString::Printf(TEXT("EaseAlpha(1) should be ~1 for type %s"), *TypeName),
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EaseType), 1.0f, 1.e-3f));
for (const float Sample : {0.0f, 0.25f, 0.5f, 0.75f, 1.0f})
{
TestTrue(FString::Printf(TEXT("EaseAlpha(%.2f) should be finite for type %s"), Sample, *TypeName),
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::EaseAlpha(Sample, EaseType)));
}
}
TestEqual("EaseAlpha should clamp alpha below 0",
UDirectiveUtilMathFunctionLibrary::EaseAlpha(-1.0f, EDirectiveUtilEaseType::BounceOut),
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EDirectiveUtilEaseType::BounceOut));
TestEqual("EaseAlpha should clamp alpha above 1",
UDirectiveUtilMathFunctionLibrary::EaseAlpha(2.0f, EDirectiveUtilEaseType::BounceOut),
UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EDirectiveUtilEaseType::BounceOut));
for (const EDirectiveUtilEaseType BounceType : {EDirectiveUtilEaseType::BounceIn, EDirectiveUtilEaseType::BounceOut, EDirectiveUtilEaseType::BounceInOut})
{
for (const float Sample : {0.1f, 0.3f, 0.6f, 0.9f})
{
const float Eased = UDirectiveUtilMathFunctionLibrary::EaseAlpha(Sample, BounceType);
TestTrue("Bounce easing should stay within [0,1]", Eased >= -1.e-3f && Eased <= 1.0f + 1.e-3f);
}
}
TestTrue("EaseFloat at alpha 0 returns A",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseFloat(10.0f, 20.0f, 0.0f, EDirectiveUtilEaseType::BounceOut), 10.0f, 1.e-3f));
TestTrue("EaseFloat at alpha 1 returns B",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseFloat(10.0f, 20.0f, 1.0f, EDirectiveUtilEaseType::BounceOut), 20.0f, 1.e-3f));
TestTrue("EaseVector at alpha 1 returns B",
UDirectiveUtilMathFunctionLibrary::EaseVector(FVector::ZeroVector, FVector(1, 2, 3), 1.0f, EDirectiveUtilEaseType::BounceOut).Equals(FVector(1, 2, 3), 1.e-2f));
TestTrue("EaseRotator at alpha 0 returns A",
UDirectiveUtilMathFunctionLibrary::EaseRotator(FRotator(10, 20, 30), FRotator(40, 50, 60), 0.0f, EDirectiveUtilEaseType::BounceOut).Equals(FRotator(10, 20, 30), 1.e-1f));
TestTrue("EaseColor at alpha 1 returns B",
UDirectiveUtilMathFunctionLibrary::EaseColor(FLinearColor::Black, FLinearColor::White, 1.0f, EDirectiveUtilEaseType::BounceOut).Equals(FLinearColor::White, 1.e-2f));
TestTrue("RoundToDecimals(3.14159, 2) ~= 3.14",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(3.14159f, 2), 3.14f, 1.e-4f));
TestTrue("RoundToDecimals(2.71828, 2) ~= 2.72 (rounds up)",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(2.71828f, 2), 2.72f, 1.e-4f));
TestTrue("RoundToDecimals(-1.2367, 2) ~= -1.24",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(-1.2367f, 2), -1.24f, 1.e-4f));
TestTrue("RoundToDecimals with 0 decimals rounds to integer",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(1.6f, 0), 2.0f, 1.e-4f));
TestTrue("RoundToDecimals(10.0, 5) ~= 10.0",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(10.0f, 5), 10.0f, 1.e-4f));
TestTrue("RoundToDecimals clamps excessive decimals without crashing",
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(1.23456789f, 50)));
TestTrue("RoundToDecimalsAsText(3.14159, 2) reads ~3.14",
FMath::IsNearlyEqual(FCString::Atof(*UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(3.14159f, 2).ToString()), 3.14f, 1.e-2f));
TestTrue("RoundToDecimals(2.5, 0) rounds half away from zero to 3",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(2.5f, 0), 3.0f, 1.e-4f));
TestTrue("RoundToDecimals(-2.5, 0) rounds half away from zero to -3",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(-2.5f, 0), -3.0f, 1.e-4f));
TestTrue("RoundToDecimals(0.125, 2) rounds half away from zero to 0.13",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(0.125f, 2), 0.13f, 0.0001f));
TestEqual("RoundToDecimalsAsText(2.5, 0) rounds half away from zero to \"3\"",
UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(2.5f, 0).ToString(), FString(TEXT("3")));
TestEqual("RoundToDecimalsAsText(-2.5, 0) rounds half away from zero to \"-3\"",
UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(-2.5f, 0).ToString(), FString(TEXT("-3")));
TestEqual("Weighted random on an empty array returns INDEX_NONE",
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(TArray<float>()), static_cast<int32>(INDEX_NONE));
TestEqual("Weighted random with all-zero weights returns INDEX_NONE",
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({0.0f, 0.0f, 0.0f}), static_cast<int32>(INDEX_NONE));
for (int32 Iteration = 0; Iteration < 25; ++Iteration)
{
TestEqual("Weighted random {0,1,0} always selects index 1",
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({0.0f, 1.0f, 0.0f}), 1);
TestEqual("Weighted random {5,0,0} always selects index 0",
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({5.0f, 0.0f, 0.0f}), 0);
}
TestEqual("FormatBytes(532) is \"532 B\"",
UDirectiveUtilMathFunctionLibrary::FormatBytes(532).ToString(), FString(TEXT("532 B")));
TestEqual("FormatBytes(1536) is \"1.5 KB\"",
UDirectiveUtilMathFunctionLibrary::FormatBytes(1536).ToString(), FString(TEXT("1.5 KB")));
TestEqual("FormatBytes(1450000, 1) is \"1.4 MB\"",
UDirectiveUtilMathFunctionLibrary::FormatBytes(1450000, 1).ToString(), FString(TEXT("1.4 MB")));
TestEqual("FormatBytes(0) is \"0 B\"",
UDirectiveUtilMathFunctionLibrary::FormatBytes(0).ToString(), FString(TEXT("0 B")));
TestEqual("FormatDuration(3785) is \"1h 03m 05s\"",
UDirectiveUtilMathFunctionLibrary::FormatDuration(3785.0f).ToString(), FString(TEXT("1h 03m 05s")));
TestEqual("FormatDuration(3785, false) is \"1h 03m\"",
UDirectiveUtilMathFunctionLibrary::FormatDuration(3785.0f, false).ToString(), FString(TEXT("1h 03m")));
TestEqual("FormatDuration(45) is \"45s\"",
UDirectiveUtilMathFunctionLibrary::FormatDuration(45.0f).ToString(), FString(TEXT("45s")));
TestEqual("FormatDuration(-90) is \"-1m 30s\"",
UDirectiveUtilMathFunctionLibrary::FormatDuration(-90.0f).ToString(), FString(TEXT("-1m 30s")));
TestEqual("FormatRelativeTime 5 minutes back reads \"5 minutes ago\"",
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromMinutes(5)).ToString(), FString(TEXT("5 minutes ago")));
TestEqual("FormatRelativeTime 10 seconds back reads \"just now\"",
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromSeconds(10)).ToString(), FString(TEXT("just now")));
TestEqual("FormatRelativeTime 2 hours ahead reads \"in 2 hours\"",
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() + FTimespan::FromHours(2)).ToString(), FString(TEXT("in 2 hours")));
TestEqual("FormatRelativeTime 1 minute back reads \"1 minute ago\"",
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromMinutes(1)).ToString(), FString(TEXT("1 minute ago")));
{
const TArray<int32> IntValues = {1, 2, 3, 4};
TestEqual("GetIntArraySum({1,2,3,4}) is 10",
UDirectiveUtilMathFunctionLibrary::GetIntArraySum(IntValues), static_cast<int64>(10));
TestTrue("GetIntArrayAverage({1,2,3,4}) is 2.5",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayAverage(IntValues), 2.5f, 1.e-4f));
TestTrue("GetIntArrayMedian({1,2,3,4}) averages the two middle values to 2.5",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(IntValues), 2.5f, 1.e-4f));
TestTrue("GetIntArrayMedian({1,2,3}) is 2",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian({1, 2, 3}), 2.0f, 1.e-4f));
TestTrue("GetIntArrayMedian handles an unsorted input",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian({3, 1, 2}), 2.0f, 1.e-4f));
TestTrue("GetIntArrayStandardDeviation({2,4,4,4,5,5,7,9}) is the population value 2",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayStandardDeviation({2, 4, 4, 4, 5, 5, 7, 9}), 2.0f, 1.e-4f));
TestEqual("GetIntArraySum({MAX_int32, MAX_int32}) does not overflow",
UDirectiveUtilMathFunctionLibrary::GetIntArraySum({MAX_int32, MAX_int32}), static_cast<int64>(MAX_int32) * 2);
const TArray<float> FloatValues = {1.0f, 2.0f, 3.0f, 4.0f};
TestTrue("GetFloatArraySum({1,2,3,4}) is 10",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArraySum(FloatValues), 10.0f, 1.e-4f));
TestTrue("GetFloatArrayAverage({1,2,3,4}) is 2.5",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayAverage(FloatValues), 2.5f, 1.e-4f));
TestTrue("GetFloatArrayMedian({1,2,3,4}) averages the two middle values to 2.5",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(FloatValues), 2.5f, 1.e-4f));
TestTrue("GetFloatArrayMedian({1,2,3}) is 2",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian({1.0f, 2.0f, 3.0f}), 2.0f, 1.e-4f));
TestTrue("GetFloatArrayStandardDeviation({2,4,4,4,5,5,7,9}) is the population value 2",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation({2.0f, 4.0f, 4.0f, 4.0f, 5.0f, 5.0f, 7.0f, 9.0f}), 2.0f, 1.e-4f));
const TArray<int32> EmptyInts;
const TArray<float> EmptyFloats;
TestEqual("GetIntArraySum of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetIntArraySum(EmptyInts), static_cast<int64>(0));
TestEqual("GetIntArrayAverage of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetIntArrayAverage(EmptyInts), 0.0f);
TestEqual("GetIntArrayMedian of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(EmptyInts), 0.0f);
TestEqual("GetIntArrayStandardDeviation of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetIntArrayStandardDeviation(EmptyInts), 0.0f);
TestEqual("GetFloatArraySum of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetFloatArraySum(EmptyFloats), 0.0f);
TestEqual("GetFloatArrayAverage of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetFloatArrayAverage(EmptyFloats), 0.0f);
TestEqual("GetFloatArrayMedian of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(EmptyFloats), 0.0f);
TestEqual("GetFloatArrayStandardDeviation of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation(EmptyFloats), 0.0f);
}
FRandomStream StreamA(12345);
FRandomStream StreamB(12345);
const int32 StreamIndexA = UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(StreamA, {1.0f, 1.0f, 1.0f, 1.0f});
const int32 StreamIndexB = UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(StreamB, {1.0f, 1.0f, 1.0f, 1.0f});
TestEqual("Weighted random from stream is deterministic for the same seed", StreamIndexA, StreamIndexB);
TestTrue("Weighted random from stream returns a valid index", StreamIndexA >= 0 && StreamIndexA < 4);
FRandomStream EmptyStream(1);
TestEqual("Weighted random from stream with all-zero weights returns INDEX_NONE",
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(EmptyStream, {0.0f, 0.0f}), static_cast<int32>(INDEX_NONE));
return true;
}