#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 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(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()), static_cast(INDEX_NONE)); TestEqual("Weighted random with all-zero weights returns INDEX_NONE", UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({0.0f, 0.0f, 0.0f}), static_cast(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 IntValues = {1, 2, 3, 4}; TestEqual("GetIntArraySum({1,2,3,4}) is 10", UDirectiveUtilMathFunctionLibrary::GetIntArraySum(IntValues), static_cast(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(MAX_int32) * 2); const TArray 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 EmptyInts; const TArray EmptyFloats; TestEqual("GetIntArraySum of an empty array is 0", UDirectiveUtilMathFunctionLibrary::GetIntArraySum(EmptyInts), static_cast(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(INDEX_NONE)); return true; }