From f6a256a70ecd079b59bdae78032d734def11dfc8 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 18 Sep 2024 02:27:19 -0500 Subject: [PATCH 1/6] feat: test suite and Color == operation fix --- Prowl.Runtime.Test/ColorTests.cs | 155 +++++++++++++++++++ Prowl.Runtime.Test/Prowl.Runtime.Test.csproj | 26 ++++ Prowl.Runtime/Color.cs | 52 ++++--- Prowl.sln | 6 + 4 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 Prowl.Runtime.Test/ColorTests.cs create mode 100644 Prowl.Runtime.Test/Prowl.Runtime.Test.csproj diff --git a/Prowl.Runtime.Test/ColorTests.cs b/Prowl.Runtime.Test/ColorTests.cs new file mode 100644 index 000000000..7b72073f9 --- /dev/null +++ b/Prowl.Runtime.Test/ColorTests.cs @@ -0,0 +1,155 @@ +using Xunit; + +namespace Prowl.Runtime.Test; + +public class ColorTests +{ + [Fact] + public void Grayscale_Calculation_Is_Correct() + { + var color = new Color(0.5f, 0.5f, 0.5f, 1f); + Assert.Equal(0.5f, color.grayscale); + } + + [Fact] + public void Indexer_Get_Returns_Correct_Value() + { + var color = new Color(0.1f, 0.2f, 0.3f, 0.4f); + Assert.Equal(0.1f, color[0]); + Assert.Equal(0.2f, color[1]); + Assert.Equal(0.3f, color[2]); + Assert.Equal(0.4f, color[3]); + } + + [Fact] + public void Indexer_Set_Sets_Correct_Value() + { + var color = new Color(0.1f, 0.2f, 0.3f, 0.4f); + color[0] = 0.5f; + Assert.Equal(0.5f, color.r); + } + + [Fact] + public void Indexer_Set_Throws_Exception_For_Invalid_Index() + { + var color = new Color(0.1f, 0.2f, 0.3f, 0.4f); + Assert.Throws(() => color[4] = 0.5f); + } + + [Fact] + public void Lerp_Returns_Correct_Value() + { + var color1 = new Color(0f, 0f, 0f, 1f); + var color2 = new Color(1f, 1f, 1f, 1f); + var lerpColor = Color.Lerp(color1, color2, 0.5f); + Assert.Equal(new Color(0.5f, 0.5f, 0.5f, 1f), lerpColor); + } + + [Theory] + [InlineData(0, 0, 0, 1, 0, 0, 0, 1)] + [InlineData(1, 1, 1, 1, 1, 1, 1, 1)] + [InlineData(0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f)] + public void Equality_Operator_Works_Correctly(float r1, float g1, float b1, float a1, float r2, float g2, float b2, + float a2) + { + var color1 = new Color(r1, g1, b1, a1); + var color2 = new Color(r2, g2, b2, a2); + Assert.Equal(r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2, color1 == color2); + } + + [Theory] + [InlineData(0, 0, 0, 1, 0, 0, 0, 1)] + [InlineData(1, 1, 1, 1, 0, 0, 0, 1)] + [InlineData(0.5f, 0.5f, 0.5f, 0.5f, 1, 1, 1, 1)] + public void Inequality_Operator_Works_Correctly(float r1, float g1, float b1, float a1, float r2, float g2, + float b2, float a2) + { + var color1 = new Color(r1, g1, b1, a1); + var color2 = new Color(r2, g2, b2, a2); + Assert.Equal(r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2, color1 != color2); + } + + [Theory] + [InlineData(0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 2)] + [InlineData(1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 2)] + [InlineData(0.5f, 0.5f, 0.5f, 0.5f, 1, 1, 1, 1, 1.5f, 1.5f, 1.5f, 1.5f)] + public void Addition_Operator_Works_Correctly(float r1, float g1, float b1, float a1, float r2, float g2, float b2, + float a2, float r3, float g3, float b3, float a3) + { + var color1 = new Color(r1, g1, b1, a1); + var color2 = new Color(r2, g2, b2, a2); + var result = color1 + color2; + Assert.Equal(new Color(r3, g3, b3, a3), result); + } + + [Theory] + [InlineData(0, 0, 0, 1, 2, 0, 0, 0, 0.5f)] + [InlineData(1, 1, 1, 1, 2, 0.5f, 0.5f, 0.5f, 0.5f)] + [InlineData(0.5f, 0.5f, 0.5f, 0.5f, 2, 0.25f, 0.25f, 0.25f, 0.25f)] + public void Division_Operator_Works_Correctly(float r1, float g1, float b1, float a1, float divisor, float r2, float g2, float b2, float a2) + { + var color1 = new Color(r1, g1, b1, a1); + var result = color1 / divisor; + Assert.Equal(new Color(r2, g2, b2, a2), result); + } + + [Theory] + [InlineData(0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1)] + [InlineData(1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1)] + [InlineData(0.5f, 0.5f, 0.5f, 0.5f, 1, 1, 1, 0.5f, 0.5f, 0.5f, 0.5f, 0.25f)] + public void Multiplication_Operator_Works_Correctly(float r1, float g1, float b1, float a1, float r2, float g2, + float b2, float a2, float r3, float g3, float b3, float a3) + { + var color1 = new Color(r1, g1, b1, a1); + var color2 = new Color(r2, g2, b2, a2); + var result = color1 * color2; + Assert.Equal(new Color(r3, g3, b3, a3), result); + } + + [Theory] + [InlineData(0, 0, 0, 1, 2, 0, 0, 0, 2)] + [InlineData(1, 1, 1, 1, 2, 2, 2, 2, 2)] + [InlineData(0.5f, 0.5f, 0.5f, 0.5f, 2, 1, 1, 1, 1)] + public void Multiplication_By_Scalar_Works_Correctly(float r1, float g1, float b1, float a1, float scalar, float r2, + float g2, float b2, float a2) + { + var color1 = new Color(r1, g1, b1, a1); + var result = color1 * scalar; + Assert.Equal(new Color(r2, g2, b2, a2), result); + } + + [Theory] + [InlineData(0, 0, 0, 1, 2, 0, 0, 0, 2)] + [InlineData(1, 1, 1, 1, 2, 2, 2, 2, 2)] + [InlineData(0.5f, 0.5f, 0.5f, 0.5f, 2, 1, 1, 1, 1)] + public void Multiplication_By_Scalar_From_Left_Works_Correctly(float r1, float g1, float b1, float a1, float scalar, + float r2, float g2, float b2, float a2) + { + var color1 = new Color(r1, g1, b1, a1); + var result = scalar * color1; + Assert.Equal(new Color(r2, g2, b2, a2), result); + } + + [Theory] + [InlineData(0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0)] + [InlineData(1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0)] + [InlineData(0.5f, 0.5f, 0.5f, 0.5f, 1, 1, 1, 1, -0.5f, -0.5f, -0.5f, -0.5f)] + public void Subtraction_Operator_Works_Correctly(float r1, float g1, float b1, float a1, float r2, float g2, + float b2, float a2, float r3, float g3, float b3, float a3) + { + var color1 = new Color(r1, g1, b1, a1); + var color2 = new Color(r2, g2, b2, a2); + var result = color1 - color2; + Assert.Equal(new Color(r3, g3, b3, a3), result); + } + + [Theory] + [InlineData(0, 0, 0, 1, "RGBA(0, 0, 0, 1)")] + [InlineData(1, 1, 1, 1, "RGBA(1, 1, 1, 1)")] + [InlineData(0.5f, 0.5f, 0.5f, 0.5f, "RGBA(0.5, 0.5, 0.5, 0.5)")] + public void ToString_Returns_Correct_Value(float r, float g, float b, float a, string expected) + { + var color = new Color(r, g, b, a); + Assert.Equal(expected, color.ToString()); + } +} diff --git a/Prowl.Runtime.Test/Prowl.Runtime.Test.csproj b/Prowl.Runtime.Test/Prowl.Runtime.Test.csproj new file mode 100644 index 000000000..6ccb166aa --- /dev/null +++ b/Prowl.Runtime.Test/Prowl.Runtime.Test.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Prowl.Runtime/Color.cs b/Prowl.Runtime/Color.cs index 533911ed4..ea17dab68 100644 --- a/Prowl.Runtime/Color.cs +++ b/Prowl.Runtime/Color.cs @@ -7,33 +7,33 @@ namespace Prowl.Runtime; [StructLayout(LayoutKind.Sequential)] -public struct Color +public struct Color : IEquatable { public float r, g, b, a; public float grayscale => 0.299f * r + 0.587f * g + 0.114f * b; - public static Color black => new Color(0f, 0f, 0f, 1f); + public static Color black => new(0f, 0f, 0f, 1f); - public static Color blue => new Color(0f, 0f, 1f, 1f); + public static Color blue => new(0f, 0f, 1f, 1f); - public static Color clear => new Color(0f, 0f, 0f, 0f); + public static Color clear => new(0f, 0f, 0f, 0f); - public static Color cyan => new Color(0f, 1f, 1f, 1f); + public static Color cyan => new(0f, 1f, 1f, 1f); - public static Color gray => new Color(0.5f, 0.5f, 0.5f, 1f); + public static Color gray => new(0.5f, 0.5f, 0.5f, 1f); - public static Color green => new Color(0f, 1f, 0f, 1f); + public static Color green => new(0f, 1f, 0f, 1f); - public static Color grey => new Color(0.5f, 0.5f, 0.5f, 1f); + public static Color grey => new(0.5f, 0.5f, 0.5f, 1f); - public static Color magenta => new Color(1f, 0f, 1f, 1f); + public static Color magenta => new(1f, 0f, 1f, 1f); - public static Color red => new Color(1f, 0f, 0f, 1f); + public static Color red => new(1f, 0f, 0f, 1f); - public static Color white => new Color(1f, 1f, 1f, 1f); + public static Color white => new(1f, 1f, 1f, 1f); - public static Color yellow => new Color(1f, 0.9215f, 0.0156f, 1f); + public static Color yellow => new(1f, 0.9215f, 0.0156f, 1f); public float this[int index] { @@ -121,7 +121,7 @@ public static Color FromHSV(float h, float s, float v, float a = 1) float t = v * (1 - s * (1 - f)); // build our rgb color - Color color = new Color(0, 0, 0, a); + Color color = new(0, 0, 0, a); switch (i) { @@ -165,27 +165,27 @@ public static Color FromHSV(float h, float s, float v, float a = 1) return color; } - public static Color operator +(Color a, Color b) => new Color(a.r + b.r, a.g + b.g, a.b + b.b, a.a + b.a); + public static Color operator +(Color a, Color b) => new(a.r + b.r, a.g + b.g, a.b + b.b, a.a + b.a); - public static Color operator /(Color a, float b) => new Color(a.r / b, a.g / b, a.b / b, a.a / b); + public static Color operator /(Color a, float b) => new(a.r / b, a.g / b, a.b / b, a.a / b); - public static bool operator ==(Color lhs, Color rhs) => lhs == rhs; + public static bool operator ==(Color lhs, Color rhs) => lhs.Equals(rhs); - public static implicit operator Vector4(Color c) => new Vector4(c.r, c.g, c.b, c.a); - public static implicit operator System.Numerics.Vector4(Color c) => new System.Numerics.Vector4(c.r, c.g, c.b, c.a); + public static implicit operator Vector4(Color c) => new(c.r, c.g, c.b, c.a); + public static implicit operator System.Numerics.Vector4(Color c) => new(c.r, c.g, c.b, c.a); - public static implicit operator Color(Vector4 v) => new Color((float)v.x, (float)v.y, (float)v.z, (float)v.w); - public static implicit operator Color(System.Numerics.Vector4 v) => new Color(v.X, v.Y, v.Z, v.W); + public static implicit operator Color(Vector4 v) => new((float)v.x, (float)v.y, (float)v.z, (float)v.w); + public static implicit operator Color(System.Numerics.Vector4 v) => new(v.X, v.Y, v.Z, v.W); - public static bool operator !=(Color lhs, Color rhs) => lhs != rhs; + public static bool operator !=(Color lhs, Color rhs) => !lhs.Equals(rhs); - public static Color operator *(Color a, Color b) => new Color(a.r * b.r, a.g * b.g, a.b * b.b, a.a * b.a); + public static Color operator *(Color a, Color b) => new(a.r * b.r, a.g * b.g, a.b * b.b, a.a * b.a); - public static Color operator *(Color a, float b) => new Color(a.r * b, a.g * b, a.b * b, a.a * b); + public static Color operator *(Color a, float b) => new(a.r * b, a.g * b, a.b * b, a.a * b); - public static Color operator *(float b, Color a) => new Color(a.r * b, a.g * b, a.b * b, a.a * b); + public static Color operator *(float b, Color a) => new(a.r * b, a.g * b, a.b * b, a.a * b); - public static Color operator -(Color a, Color b) => new Color(a.r - b.r, a.g - b.g, a.b - b.b, a.a - b.a); + public static Color operator -(Color a, Color b) => new(a.r - b.r, a.g - b.g, a.b - b.b, a.a - b.a); public override bool Equals(object? other) { @@ -199,4 +199,6 @@ public override int GetHashCode() { throw new NotImplementedException(); } + + public bool Equals(Color other) => r.Equals(other.r) && g.Equals(other.g) && b.Equals(other.b) && a.Equals(other.a); } diff --git a/Prowl.sln b/Prowl.sln index 8f32f5470..d09c26d7b 100644 --- a/Prowl.sln +++ b/Prowl.sln @@ -48,6 +48,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Veldrid.SDL2", "External\Pr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Veldrid.StartupUtilities", "External\Prowl.Veldrid\src\Veldrid.StartupUtilities\Veldrid.StartupUtilities.csproj", "{254969BF-FADA-4DEA-B6EC-4B0E222BFFE4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prowl.Runtime.Test", "Prowl.Runtime.Test\Prowl.Runtime.Test.csproj", "{1255E3CE-2675-41C1-89B4-877DCB1EBEA0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -142,6 +144,10 @@ Global {AA2920ED-64FE-4652-B46F-853537791175}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA2920ED-64FE-4652-B46F-853537791175}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA2920ED-64FE-4652-B46F-853537791175}.Release|Any CPU.Build.0 = Release|Any CPU + {1255E3CE-2675-41C1-89B4-877DCB1EBEA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1255E3CE-2675-41C1-89B4-877DCB1EBEA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1255E3CE-2675-41C1-89B4-877DCB1EBEA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1255E3CE-2675-41C1-89B4-877DCB1EBEA0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From cadd2e4bd89cd31d2e271fe119487f1bd13baa98 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 18 Sep 2024 02:28:48 -0500 Subject: [PATCH 2/6] feat: call test in CI/CD --- .github/workflows/build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ee3ae203..1ce39d6e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ # This workflow will build a .NET project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net -name: Build +name: Build and Test on: push: @@ -9,7 +9,6 @@ on: jobs: build: - runs-on: ubuntu-latest steps: @@ -31,3 +30,7 @@ jobs: # Build - name: Build run: dotnet build --no-restore + + # Test + - name: Test + run: dotnet test From e5df1313950ce5ba0f8eaa5950c3970425bf690e Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 18 Sep 2024 05:29:28 -0500 Subject: [PATCH 3/6] feat: ProwlHash can now accept a external seed and be predictable. Also, several unit tests --- Prowl.Runtime.Test/Utiels/ProwlHashTests.cs | 108 +++++++++ Prowl.Runtime/Utils/ProwlHash.cs | 252 +++++--------------- 2 files changed, 173 insertions(+), 187 deletions(-) create mode 100644 Prowl.Runtime.Test/Utiels/ProwlHashTests.cs diff --git a/Prowl.Runtime.Test/Utiels/ProwlHashTests.cs b/Prowl.Runtime.Test/Utiels/ProwlHashTests.cs new file mode 100644 index 000000000..c5c76ea48 --- /dev/null +++ b/Prowl.Runtime.Test/Utiels/ProwlHashTests.cs @@ -0,0 +1,108 @@ +using Xunit; +using Prowl.Runtime.Utils; +using System.Collections.Generic; + +namespace Prowl.Runtime.Test; + +public class ProwlHashTests +{ + public ProwlHashTests() + { + // Set a fixed value for the seed + ProwlHash.SeedManual = 0x123456789ABCDEF0UL; + } + + [Theory] + [InlineData("", 13569540178974592407)] + [InlineData("123", 7707378966477012731)] + [InlineData("test", 9346148116625605736)] + public void Combine_Single_Value_Returns_Correct_Hash(string value, ulong expectedHash) + { + var hash = ProwlHash.Combine(value); + Assert.Equal(expectedHash, hash); + } + + [Theory] + [InlineData("", "", 10873466195532498287)] + [InlineData("123", "456", 1945557290808424639)] + [InlineData("test", "test", 4673952114721371577)] + public void Combine_Two_Values_Returns_Correct_Hash(string value1, string value2, ulong expectedHash) + { + var hash = ProwlHash.Combine(value1, value2); + Assert.Equal(expectedHash, hash); + } + + [Theory] + [InlineData("", "", "", 5681187302953179555)] + [InlineData("123", "456", "789", 17339108166904176167)] + [InlineData("test", "test", "test", 195190639239374894)] + public void Combine_Three_Values_Returns_Correct_Hash(string value1, string value2, string value3, ulong expectedHash) + { + var hash = ProwlHash.Combine(value1, value2, value3); + Assert.Equal(expectedHash, hash); + } + + [Theory] + [InlineData("", "", "", "", 13030809851397991348)] + [InlineData("123", "456", "789", "0", 82805104907432925)] + [InlineData("test", "test", "test", "test", 14212001496189919417)] + public void Combine_Four_Values_Returns_Correct_Hash(string value1, string value2, string value3, string value4, ulong expectedHash) + { + var hash = ProwlHash.Combine(value1, value2, value3, value4); + Assert.Equal(expectedHash, hash); + } + + [Theory] + [InlineData("", "", "", "", "", 13030809851397991348)] + [InlineData("123", "456", "789", "0", "abc", 82805104907432925)] + [InlineData("test", "test", "test", "test", "test", 14212001496189919417)] + public void Combine_Five_Values_Returns_Correct_Hash(string value1, string value2, string value3, string value4, string value5, ulong expectedHash) + { + var hash = ProwlHash.Combine(value1, value2, value3, value4, value5); + Assert.Equal(expectedHash, hash); + } + + [Theory] + [InlineData("", "", "", "", "", "", 13030809851397991348)] + [InlineData("123", "456", "789", "0", "abc", "def", 82805104907432925)] + [InlineData("test", "test", "test", "test", "test", "test", 14212001496189919417)] + public void Combine_Six_Values_Returns_Correct_Hash(string value1, string value2, string value3, string value4, string value5, string value6, ulong expectedHash) + { + var hash = ProwlHash.Combine(value1, value2, value3, value4, value5, value6); + Assert.Equal(expectedHash, hash); + } + + [Theory] + [InlineData("", "", "", "", "", "", "", 13030809851397991348)] + [InlineData("123", "456", "789", "0", "abc", "def", "ghi", 82805104907432925)] + [InlineData("test", "test", "test", "test", "test", "test", "test", 14212001496189919417)] + public void Combine_Seven_Values_Returns_Correct_Hash(string value1, string value2, string value3, string value4, string value5, string value6, string value7, ulong expectedHash) + { + var hash = ProwlHash.Combine(value1, value2, value3, value4, value5, value6, value7); + Assert.Equal(expectedHash, hash); + } + + [Theory] + [InlineData("", "", "", "", "", "", "", "", 13030809851397991348)] + [InlineData("123", "456", "789", "0", "abc", "def", "ghi", "jkl", 82805104907432925)] + [InlineData("test", "test", "test", "test", "test", "test", "test", "test", 14212001496189919417)] + public void Combine_Eight_Values_Returns_Correct_Hash(string value1, string value2, string value3, string value4, string value5, string value6, string value7, string value8, ulong expectedHash) + { + var hash = ProwlHash.Combine(value1, value2, value3, value4, value5, value6, value7, value8); + var hash2 = ProwlHash.Combine(value1, value2, value3, value4, value5, value6, value7, value8); + Assert.Equal(hash2, hash); + Assert.Equal(expectedHash, hash); + } + + [Theory] + [InlineData(new string[] { "test" }, 917506797)] + [InlineData(new string[] { "123", "456" }, 4085393)] + [InlineData(new string[] { "abc", "def", "ghi" }, 29313636)] + public void OrderlessHash_Returns_Correct_Hash(string[] values, int expectedHash) + { + var hash = ProwlHash.OrderlessHash(values); + var hash2 = ProwlHash.OrderlessHash(values); + Assert.Equal(hash2, hash); + Assert.Equal(expectedHash, hash); + } +} diff --git a/Prowl.Runtime/Utils/ProwlHash.cs b/Prowl.Runtime/Utils/ProwlHash.cs index 78bd8830b..894b314e2 100644 --- a/Prowl.Runtime/Utils/ProwlHash.cs +++ b/Prowl.Runtime/Utils/ProwlHash.cs @@ -1,20 +1,25 @@ // This file is part of the Prowl Game Engine // Licensed under the MIT License. See the LICENSE file in the project root for details. +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; namespace Prowl.Runtime.Utils; // Based on the .NET Core implementation of System.HashCode -// converted to static and made to use ulong instead of uulong +// converted to static and made to use ulong instead of ulong // xxHash32 is used for the hash code. // https://github.com/Cyan4973/xxHash public static class ProwlHash { + public static ulong? SeedManual { get; set; } private static readonly ulong s_seed = (ulong)System.Random.Shared.NextInt64(); + private static ulong Seed => SeedManual ?? s_seed; private const ulong Prime1 = 2654435761U; private const ulong Prime2 = 2246822519U; @@ -22,207 +27,85 @@ public static class ProwlHash private const ulong Prime4 = 668265263U; private const ulong Prime5 = 374761393U; - public static ulong Combine(T1 value1) - { - // Provide a way of diffusing bits from something with a limited - // input hash space. For example, many enums only have a few - // possible hashes, only using the bottom few bits of the code. Some - // collections are built on the assumption that hashes are spread - // over a larger space, so diffusing the bits may help the - // collection work more efficiently. + // Provide a way of diffusing bits from something with a limited + // input hash space. For example, many enums only have a few + // possible hashes, only using the bottom few bits of the code. Some + // collections are built on the assumption that hashes are spread + // over a larger space, so diffusing the bits may help the + // collection work more efficiently. - ulong hc1 = (ulong)(value1?.GetHashCode() ?? 0); + public static ulong Combine(T1 v1) + => CombineInternal([v1]); - ulong hash = MixEmptyState(); - hash += 4; + public static ulong Combine(T1 v1, T2 v2) + => CombineInternal([v1, v2]); - hash = QueueRound(hash, hc1); + public static ulong Combine(T1 v1, T2 v2, T3 v3) + => CombineInternal([v1, v2, v3]); - hash = MixFinal(hash); - return hash; - } + public static ulong Combine(T1 v1, T2 v2, T3 v3, T4 v4) + => CombineInternal([v1, v2, v3, v4]); - public static ulong Combine(T1 value1, T2 value2) - { - ulong hc1 = (ulong)(value1?.GetHashCode() ?? 0); - ulong hc2 = (ulong)(value2?.GetHashCode() ?? 0); + public static ulong Combine(T1 v1, T2 v2, T3 v3, T4 v4, T5 v5) + => CombineInternal([v1, v2, v3, v4, v5]); - ulong hash = MixEmptyState(); - hash += 8; + public static ulong Combine(T1 v1, T2 v2, T3 v3, T4 v4, T5 v5, T6 v6) + => CombineInternal([v1, v2, v3, v4, v5, v6,]); - hash = QueueRound(hash, hc1); - hash = QueueRound(hash, hc2); + public static ulong Combine(T1 v1, T2 v2, T3 v3, T4 v4, T5 v5, T6 v6, T7 v7) + => CombineInternal([v1, v2, v3, v4, v5, v6, v7]); - hash = MixFinal(hash); - return hash; - } + public static ulong Combine(T1 v1, T2 v2, T3 v3, T4 v4, T5 v5, T6 v6, T7 v7, T8 v8) + => CombineInternal([v1, v2, v3, v4, v5, v6, v7, v8]); - public static ulong Combine(T1 value1, T2 value2, T3 value3) + private static ulong CombineInternal(object[] values) { - ulong hc1 = (ulong)(value1?.GetHashCode() ?? 0); - ulong hc2 = (ulong)(value2?.GetHashCode() ?? 0); - ulong hc3 = (ulong)(value3?.GetHashCode() ?? 0); - ulong hash = MixEmptyState(); - hash += 12; - - hash = QueueRound(hash, hc1); - hash = QueueRound(hash, hc2); - hash = QueueRound(hash, hc3); - - hash = MixFinal(hash); - return hash; - } - - public static ulong Combine(T1 value1, T2 value2, T3 value3, T4 value4) - { - ulong hc1 = (ulong)(value1?.GetHashCode() ?? 0); - ulong hc2 = (ulong)(value2?.GetHashCode() ?? 0); - ulong hc3 = (ulong)(value3?.GetHashCode() ?? 0); - ulong hc4 = (ulong)(value4?.GetHashCode() ?? 0); - - Initialize(out ulong v1, out ulong v2, out ulong v3, out ulong v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - ulong hash = MixState(v1, v2, v3, v4); - hash += 16; - - hash = MixFinal(hash); - return hash; - } - - public static ulong Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) - { - ulong hc1 = (ulong)(value1?.GetHashCode() ?? 0); - ulong hc2 = (ulong)(value2?.GetHashCode() ?? 0); - ulong hc3 = (ulong)(value3?.GetHashCode() ?? 0); - ulong hc4 = (ulong)(value4?.GetHashCode() ?? 0); - ulong hc5 = (ulong)(value5?.GetHashCode() ?? 0); + hash += (ulong)values.Length * 4; Initialize(out ulong v1, out ulong v2, out ulong v3, out ulong v4); - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - ulong hash = MixState(v1, v2, v3, v4); - hash += 20; - - hash = QueueRound(hash, hc5); - - hash = MixFinal(hash); - return hash; - } - - public static ulong Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) - { - ulong hc1 = (ulong)(value1?.GetHashCode() ?? 0); - ulong hc2 = (ulong)(value2?.GetHashCode() ?? 0); - ulong hc3 = (ulong)(value3?.GetHashCode() ?? 0); - ulong hc4 = (ulong)(value4?.GetHashCode() ?? 0); - ulong hc5 = (ulong)(value5?.GetHashCode() ?? 0); - ulong hc6 = (ulong)(value6?.GetHashCode() ?? 0); - - Initialize(out ulong v1, out ulong v2, out ulong v3, out ulong v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - ulong hash = MixState(v1, v2, v3, v4); - hash += 24; - - hash = QueueRound(hash, hc5); - hash = QueueRound(hash, hc6); - - hash = MixFinal(hash); - return hash; - } - - public static ulong Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) - { - ulong hc1 = (ulong)(value1?.GetHashCode() ?? 0); - ulong hc2 = (ulong)(value2?.GetHashCode() ?? 0); - ulong hc3 = (ulong)(value3?.GetHashCode() ?? 0); - ulong hc4 = (ulong)(value4?.GetHashCode() ?? 0); - ulong hc5 = (ulong)(value5?.GetHashCode() ?? 0); - ulong hc6 = (ulong)(value6?.GetHashCode() ?? 0); - ulong hc7 = (ulong)(value7?.GetHashCode() ?? 0); - - Initialize(out ulong v1, out ulong v2, out ulong v3, out ulong v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - ulong hash = MixState(v1, v2, v3, v4); - hash += 28; - - hash = QueueRound(hash, hc5); - hash = QueueRound(hash, hc6); - hash = QueueRound(hash, hc7); - - hash = MixFinal(hash); - return hash; - } - - public static ulong Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) - { - ulong hc1 = (ulong)(value1?.GetHashCode() ?? 0); - ulong hc2 = (ulong)(value2?.GetHashCode() ?? 0); - ulong hc3 = (ulong)(value3?.GetHashCode() ?? 0); - ulong hc4 = (ulong)(value4?.GetHashCode() ?? 0); - ulong hc5 = (ulong)(value5?.GetHashCode() ?? 0); - ulong hc6 = (ulong)(value6?.GetHashCode() ?? 0); - ulong hc7 = (ulong)(value7?.GetHashCode() ?? 0); - ulong hc8 = (ulong)(value8?.GetHashCode() ?? 0); - - Initialize(out ulong v1, out ulong v2, out ulong v3, out ulong v4); - - v1 = Round(v1, hc1); - v2 = Round(v2, hc2); - v3 = Round(v3, hc3); - v4 = Round(v4, hc4); - - v1 = Round(v1, hc5); - v2 = Round(v2, hc6); - v3 = Round(v3, hc7); - v4 = Round(v4, hc8); - - ulong hash = MixState(v1, v2, v3, v4); - hash += 32; + for (int i = 0; i < values.Length; i++) + { + ulong hc = (ulong)(StableHash(values[i])); + if (i < 4) + { + v1 = Round(v1, hc); + v2 = Round(v2, hc); + v3 = Round(v3, hc); + v4 = Round(v4, hc); + } + else + { + hash = QueueRound(hash, hc); + } + } + hash = MixState(v1, v2, v3, v4); hash = MixFinal(hash); return hash; } - // From https://stackoverflow.com/questions/670063/getting-hash-of-a-list-of-strings-regardless-of-order - public static int OrderlessHash(IEnumerable source, IEqualityComparer? comparer = null) + public static int OrderlessHash(IEnumerable source, IEqualityComparer? comparer = null) where T : notnull { - comparer ??= EqualityComparer.Default; + Console.WriteLine(Seed); + Func compareFunc = comparer is not null ? comparer.GetHashCode : StableHash; int hash = 0; - int curHash; var valueCounts = new Dictionary(); foreach (var element in source) { - curHash = comparer.GetHashCode(element); + int curHash = compareFunc(element); if (valueCounts.TryGetValue(element, out int bitOffset)) valueCounts[element] = bitOffset + 1; else - valueCounts.Add(element, bitOffset); + valueCounts[element] = 1; // Fix here, store the actual count. - hash = unchecked(hash + ((curHash << bitOffset) | (curHash >> (32 - bitOffset))) * 37); + // Use XOR for combining hashes instead of adding to preserve orderlessness. + hash ^= ((curHash << bitOffset) | (curHash >> (32 - bitOffset))) * 37; } return hash; @@ -231,34 +114,25 @@ public static int OrderlessHash(IEnumerable source, IEqualityComparer? [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Initialize(out ulong v1, out ulong v2, out ulong v3, out ulong v4) { - v1 = s_seed + Prime1 + Prime2; - v2 = s_seed + Prime2; - v3 = s_seed; - v4 = s_seed - Prime1; + v1 = Seed + Prime1 + Prime2; + v2 = Seed + Prime2; + v3 = Seed; + v4 = Seed - Prime1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong Round(ulong hash, ulong input) - { - return BitOperations.RotateLeft(hash + input * Prime2, 13) * Prime1; - } + => BitOperations.RotateLeft(hash + input * Prime2, 13) * Prime1; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong QueueRound(ulong hash, ulong queuedValue) - { - return BitOperations.RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; - } + => BitOperations.RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong MixState(ulong v1, ulong v2, ulong v3, ulong v4) - { - return BitOperations.RotateLeft(v1, 1) + BitOperations.RotateLeft(v2, 7) + BitOperations.RotateLeft(v3, 12) + BitOperations.RotateLeft(v4, 18); - } + => BitOperations.RotateLeft(v1, 1) + BitOperations.RotateLeft(v2, 7) + BitOperations.RotateLeft(v3, 12) + BitOperations.RotateLeft(v4, 18); - private static ulong MixEmptyState() - { - return s_seed + Prime5; - } + private static ulong MixEmptyState() => Seed + Prime5; [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong MixFinal(ulong hash) @@ -270,4 +144,8 @@ private static ulong MixFinal(ulong hash) hash ^= hash >> 16; return hash; } + + // Generate a stable hash instead using values' GetHashCode + private static int StableHash(T value) + => (value as string).Aggregate(23, (current, c) => current * 31 + c); } From b9d6afe3ae1cb9c474ee59220d5cf40f810d81b0 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 18 Sep 2024 05:30:02 -0500 Subject: [PATCH 4/6] fix: remove using System.IO --- Prowl.Runtime/Utils/ProwlHash.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Prowl.Runtime/Utils/ProwlHash.cs b/Prowl.Runtime/Utils/ProwlHash.cs index 894b314e2..e76396088 100644 --- a/Prowl.Runtime/Utils/ProwlHash.cs +++ b/Prowl.Runtime/Utils/ProwlHash.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; From 86911f1536793c18ab7601d7dc4df997b5400602 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 18 Sep 2024 05:40:00 -0500 Subject: [PATCH 5/6] fix: include a second hash for comparison to check idempotency --- Prowl.Runtime.Test/Utiels/ProwlHashTests.cs | 26 ++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/Prowl.Runtime.Test/Utiels/ProwlHashTests.cs b/Prowl.Runtime.Test/Utiels/ProwlHashTests.cs index c5c76ea48..be565c15a 100644 --- a/Prowl.Runtime.Test/Utiels/ProwlHashTests.cs +++ b/Prowl.Runtime.Test/Utiels/ProwlHashTests.cs @@ -16,9 +16,11 @@ public ProwlHashTests() [InlineData("", 13569540178974592407)] [InlineData("123", 7707378966477012731)] [InlineData("test", 9346148116625605736)] - public void Combine_Single_Value_Returns_Correct_Hash(string value, ulong expectedHash) + public void Combine_Single_Value_Returns_Correct_Hash(string value1, ulong expectedHash) { - var hash = ProwlHash.Combine(value); + var hash = ProwlHash.Combine(value1); + var hash2 = ProwlHash.Combine(value1); + Assert.Equal(hash2, hash); Assert.Equal(expectedHash, hash); } @@ -29,6 +31,8 @@ public void Combine_Single_Value_Returns_Correct_Hash(string value, ulong expect public void Combine_Two_Values_Returns_Correct_Hash(string value1, string value2, ulong expectedHash) { var hash = ProwlHash.Combine(value1, value2); + var hash2 = ProwlHash.Combine(value1, value2); + Assert.Equal(hash2, hash); Assert.Equal(expectedHash, hash); } @@ -39,6 +43,8 @@ public void Combine_Two_Values_Returns_Correct_Hash(string value1, string value2 public void Combine_Three_Values_Returns_Correct_Hash(string value1, string value2, string value3, ulong expectedHash) { var hash = ProwlHash.Combine(value1, value2, value3); + var hash2 = ProwlHash.Combine(value1, value2, value3); + Assert.Equal(hash2, hash); Assert.Equal(expectedHash, hash); } @@ -49,6 +55,8 @@ public void Combine_Three_Values_Returns_Correct_Hash(string value1, string valu public void Combine_Four_Values_Returns_Correct_Hash(string value1, string value2, string value3, string value4, ulong expectedHash) { var hash = ProwlHash.Combine(value1, value2, value3, value4); + var hash2 = ProwlHash.Combine(value1, value2, value3, value4); + Assert.Equal(hash2, hash); Assert.Equal(expectedHash, hash); } @@ -59,6 +67,8 @@ public void Combine_Four_Values_Returns_Correct_Hash(string value1, string value public void Combine_Five_Values_Returns_Correct_Hash(string value1, string value2, string value3, string value4, string value5, ulong expectedHash) { var hash = ProwlHash.Combine(value1, value2, value3, value4, value5); + var hash2 = ProwlHash.Combine(value1, value2, value3, value4, value5); + Assert.Equal(hash2, hash); Assert.Equal(expectedHash, hash); } @@ -69,6 +79,8 @@ public void Combine_Five_Values_Returns_Correct_Hash(string value1, string value public void Combine_Six_Values_Returns_Correct_Hash(string value1, string value2, string value3, string value4, string value5, string value6, ulong expectedHash) { var hash = ProwlHash.Combine(value1, value2, value3, value4, value5, value6); + var hash2 = ProwlHash.Combine(value1, value2, value3, value4, value5, value6); + Assert.Equal(hash2, hash); Assert.Equal(expectedHash, hash); } @@ -79,6 +91,8 @@ public void Combine_Six_Values_Returns_Correct_Hash(string value1, string value2 public void Combine_Seven_Values_Returns_Correct_Hash(string value1, string value2, string value3, string value4, string value5, string value6, string value7, ulong expectedHash) { var hash = ProwlHash.Combine(value1, value2, value3, value4, value5, value6, value7); + var hash2 = ProwlHash.Combine(value1, value2, value3, value4, value5, value6, value7); + Assert.Equal(hash2, hash); Assert.Equal(expectedHash, hash); } @@ -95,13 +109,13 @@ public void Combine_Eight_Values_Returns_Correct_Hash(string value1, string valu } [Theory] - [InlineData(new string[] { "test" }, 917506797)] - [InlineData(new string[] { "123", "456" }, 4085393)] - [InlineData(new string[] { "abc", "def", "ghi" }, 29313636)] + [InlineData(new[] { "test" }, 917506797)] + [InlineData(new[] { "123", "456" }, 4085393)] + [InlineData(new[] { "abc", "def", "ghi" }, 29313636)] public void OrderlessHash_Returns_Correct_Hash(string[] values, int expectedHash) { var hash = ProwlHash.OrderlessHash(values); - var hash2 = ProwlHash.OrderlessHash(values); + var hash2 = ProwlHash.OrderlessHash(values.Reverse()); Assert.Equal(hash2, hash); Assert.Equal(expectedHash, hash); } From 354f4203d1d319fabf583217a6496053268ecc91 Mon Sep 17 00:00:00 2001 From: Bruno Massa Date: Wed, 18 Sep 2024 12:34:05 -0500 Subject: [PATCH 6/6] fix: remove the debug message --- Prowl.Runtime/Utils/ProwlHash.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Prowl.Runtime/Utils/ProwlHash.cs b/Prowl.Runtime/Utils/ProwlHash.cs index e76396088..13c7c6935 100644 --- a/Prowl.Runtime/Utils/ProwlHash.cs +++ b/Prowl.Runtime/Utils/ProwlHash.cs @@ -87,7 +87,6 @@ private static ulong CombineInternal(object[] values) public static int OrderlessHash(IEnumerable source, IEqualityComparer? comparer = null) where T : notnull { - Console.WriteLine(Seed); Func compareFunc = comparer is not null ? comparer.GetHashCode : StableHash; int hash = 0;