diff --git a/packages/cultmath/tests/CultMath.Tests/VectorEqualityTests.cs b/packages/cultmath/tests/CultMath.Tests/VectorEqualityTests.cs new file mode 100644 index 00000000..8f898b22 --- /dev/null +++ b/packages/cultmath/tests/CultMath.Tests/VectorEqualityTests.cs @@ -0,0 +1,483 @@ +using System; +using CultMath; +using Xunit; + +namespace CultMath.Tests; + +// Hand-verified on hands/adopt-stryker: weakening float3.Equals from `&&` to `||` passes the +// whole existing suite. The same shape of Equals/GetHashCode, ==/!=, and /<=/>= exists (with +// no shared base type) across float2/3/4, double2/3, int2/3/4, bool2/3/4, quaternion, +// float2x2 and float3x3. These tests are data-driven per component position rather than one +// method per type: each type supplies a base value and, per component index, a value that +// differs at exactly that index and agrees everywhere else, plus (for the four ordered numeric +// families) a left/right pair whose relational result differs per component. bool2/3/4 and +// quaternion have no ordering operators and are not included in the comparison-operator cases. +// float2x2/float3x3 have no == / != operators (Equals and GetHashCode only) and are covered +// separately, at row granularity. +public sealed class VectorEqualityTests +{ + // ---- shared, type-agnostic assertions ------------------------------------------------- + + private static void AssertEqualsAndHashCodeContract(T a, T b, bool expectedEqual, string label) + where T : IEquatable + { + Assert.True(a.Equals(b) == expectedEqual, $"{label}: a.Equals(b) expected {expectedEqual}"); + Assert.True(b.Equals(a) == expectedEqual, $"{label}: b.Equals(a) expected {expectedEqual} (symmetry)"); + if (expectedEqual) + { + Assert.True(a.GetHashCode() == b.GetHashCode(), $"{label}: equal values must share a hash code"); + } + } + + private static void AssertElementwiseEqualityOperators( + TLeft a, + TRight b, + int count, + Func expectedEqualAt, + Func eq, + Func neq, + Func at, + string label) + { + var eqResult = eq(a, b); + var neqResult = neq(a, b); + for (var i = 0; i < count; i++) + { + var expectedEqual = expectedEqualAt(i); + Assert.True(at(eqResult, i) == expectedEqual, $"{label}: == component {i} expected {expectedEqual}"); + Assert.True(at(neqResult, i) == !expectedEqual, $"{label}: != component {i} expected {!expectedEqual}"); + } + } + + private static void AssertElementwiseComparisonOperators( + TLeft left, + TRight right, + double[] leftVals, + double[] rightVals, + Func lt, + Func gt, + Func le, + Func ge, + Func at, + string label) + { + var ltResult = lt(left, right); + var gtResult = gt(left, right); + var leResult = le(left, right); + var geResult = ge(left, right); + for (var i = 0; i < leftVals.Length; i++) + { + var less = leftVals[i] < rightVals[i]; + var greater = leftVals[i] > rightVals[i]; + Assert.True(at(ltResult, i) == less, $"{label}: < component {i} (left={leftVals[i]}, right={rightVals[i]})"); + Assert.True(at(gtResult, i) == greater, $"{label}: > component {i} (left={leftVals[i]}, right={rightVals[i]})"); + Assert.True(at(leResult, i) == !greater, $"{label}: <= component {i} (left={leftVals[i]}, right={rightVals[i]})"); + Assert.True(at(geResult, i) == !less, $"{label}: >= component {i} (left={leftVals[i]}, right={rightVals[i]})"); + } + } + + // ---- float2 ----------------------------------------------------------------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + public void Float2EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new float2(3.0f, -7.0f); + var b = a; + b[index] += 5.0f; + + AssertEqualsAndHashCodeContract(a, a, true, "float2 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"float2 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 2, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float2 vector==vector"); + + // Scalar overloads: a scalar equal to exactly one component, on both operand sides. + var scalar = a[index]; + AssertElementwiseEqualityOperators( + a, scalar, 2, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float2 vector==scalar"); + AssertElementwiseEqualityOperators( + scalar, a, 2, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float2 scalar==vector"); + } + + [Fact] + public void Float2ComparisonOperatorsDifferPerComponent() + { + var left = new float2(1.0f, 5.0f); + var right = new float2(2.0f, 3.0f); + var leftVals = new double[] { 1.0, 5.0 }; + var rightVals = new double[] { 2.0, 3.0 }; + + AssertElementwiseComparisonOperators( + left, right, leftVals, rightVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float2 vector-vector"); + + const float scalar = 2.0f; + AssertElementwiseComparisonOperators( + left, scalar, leftVals, new double[] { scalar, scalar }, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float2 vector-scalar"); + AssertElementwiseComparisonOperators( + scalar, left, new double[] { scalar, scalar }, leftVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float2 scalar-vector"); + } + + // ---- float3 ----------------------------------------------------------------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void Float3EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new float3(3.0f, -7.0f, 4.5f); + var b = a; + b[index] += 5.0f; + + AssertEqualsAndHashCodeContract(a, a, true, "float3 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"float3 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 3, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float3 vector==vector"); + + var scalar = a[index]; + AssertElementwiseEqualityOperators( + a, scalar, 3, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float3 vector==scalar"); + AssertElementwiseEqualityOperators( + scalar, a, 3, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float3 scalar==vector"); + } + + [Fact] + public void Float3ComparisonOperatorsDifferPerComponent() + { + var left = new float3(1.0f, 5.0f, 4.0f); + var right = new float3(2.0f, 3.0f, 4.0f); + var leftVals = new double[] { 1.0, 5.0, 4.0 }; + var rightVals = new double[] { 2.0, 3.0, 4.0 }; + + AssertElementwiseComparisonOperators( + left, right, leftVals, rightVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float3 vector-vector"); + + const float scalar = 4.0f; + AssertElementwiseComparisonOperators( + left, scalar, leftVals, new double[] { scalar, scalar, scalar }, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float3 vector-scalar"); + AssertElementwiseComparisonOperators( + scalar, left, new double[] { scalar, scalar, scalar }, leftVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float3 scalar-vector"); + } + + // ---- float4 ----------------------------------------------------------------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void Float4EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new float4(3.0f, -7.0f, 4.5f, 9.0f); + var b = a; + b[index] += 5.0f; + + AssertEqualsAndHashCodeContract(a, a, true, "float4 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"float4 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 4, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float4 vector==vector"); + + var scalar = a[index]; + AssertElementwiseEqualityOperators( + a, scalar, 4, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float4 vector==scalar"); + AssertElementwiseEqualityOperators( + scalar, a, 4, i => a[i] == scalar, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "float4 scalar==vector"); + } + + [Fact] + public void Float4ComparisonOperatorsDifferPerComponent() + { + var left = new float4(1.0f, 5.0f, 4.0f, 8.0f); + var right = new float4(2.0f, 3.0f, 4.0f, 6.0f); + var leftVals = new double[] { 1.0, 5.0, 4.0, 8.0 }; + var rightVals = new double[] { 2.0, 3.0, 4.0, 6.0 }; + + AssertElementwiseComparisonOperators( + left, right, leftVals, rightVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float4 vector-vector"); + + const float scalar = 4.0f; + AssertElementwiseComparisonOperators( + left, scalar, leftVals, new double[] { scalar, scalar, scalar, scalar }, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float4 vector-scalar"); + AssertElementwiseComparisonOperators( + scalar, left, new double[] { scalar, scalar, scalar, scalar }, leftVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "float4 scalar-vector"); + } + + // ---- double2 ---------------------------------------------------------------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + public void Double2EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new double2(3.0, -7.0); + var b = a; + b[index] += 5.0; + + AssertEqualsAndHashCodeContract(a, a, true, "double2 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"double2 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 2, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "double2 =="); + } + + [Fact] + public void Double2ComparisonOperatorsDifferPerComponent() + { + var left = new double2(1.0, 5.0); + var right = new double2(2.0, 3.0); + var leftVals = new double[] { 1.0, 5.0 }; + var rightVals = new double[] { 2.0, 3.0 }; + + AssertElementwiseComparisonOperators( + left, right, leftVals, rightVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "double2 vector-vector"); + } + + // ---- double3 ---------------------------------------------------------------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void Double3EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new double3(3.0, -7.0, 4.5); + var b = a; + b[index] += 5.0; + + AssertEqualsAndHashCodeContract(a, a, true, "double3 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"double3 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 3, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "double3 =="); + } + + [Fact] + public void Double3ComparisonOperatorsDifferPerComponent() + { + var left = new double3(1.0, 5.0, 4.0); + var right = new double3(2.0, 3.0, 4.0); + var leftVals = new double[] { 1.0, 5.0, 4.0 }; + var rightVals = new double[] { 2.0, 3.0, 4.0 }; + + AssertElementwiseComparisonOperators( + left, right, leftVals, rightVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "double3 vector-vector"); + } + + // ---- int2 ------------------------------------------------------------------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + public void Int2EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new int2(3, -7); + var b = a; + b[index] += 5; + + AssertEqualsAndHashCodeContract(a, a, true, "int2 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"int2 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 2, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "int2 =="); + } + + [Fact] + public void Int2ComparisonOperatorsDifferPerComponent() + { + var left = new int2(1, 5); + var right = new int2(2, 3); + var leftVals = new double[] { 1, 5 }; + var rightVals = new double[] { 2, 3 }; + + AssertElementwiseComparisonOperators( + left, right, leftVals, rightVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "int2 vector-vector"); + } + + // ---- int3 ------------------------------------------------------------------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void Int3EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new int3(3, -7, 4); + var b = a; + b[index] += 5; + + AssertEqualsAndHashCodeContract(a, a, true, "int3 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"int3 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 3, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "int3 =="); + } + + [Fact] + public void Int3ComparisonOperatorsDifferPerComponent() + { + var left = new int3(1, 5, 4); + var right = new int3(2, 3, 4); + var leftVals = new double[] { 1, 5, 4 }; + var rightVals = new double[] { 2, 3, 4 }; + + AssertElementwiseComparisonOperators( + left, right, leftVals, rightVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "int3 vector-vector"); + } + + // ---- int4 ------------------------------------------------------------------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void Int4EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new int4(3, -7, 4, 9); + var b = a; + b[index] += 5; + + AssertEqualsAndHashCodeContract(a, a, true, "int4 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"int4 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 4, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "int4 =="); + } + + [Fact] + public void Int4ComparisonOperatorsDifferPerComponent() + { + var left = new int4(1, 5, 4, 8); + var right = new int4(2, 3, 4, 6); + var leftVals = new double[] { 1, 5, 4, 8 }; + var rightVals = new double[] { 2, 3, 4, 6 }; + + AssertElementwiseComparisonOperators( + left, right, leftVals, rightVals, + (x, y) => x < y, (x, y) => x > y, (x, y) => x <= y, (x, y) => x >= y, (v, i) => v[i], "int4 vector-vector"); + } + + // ---- bool2/3/4 (no ordering operators) --------------------------------------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + public void Bool2EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new bool2(true, false); + var b = a; + b[index] = !b[index]; + + AssertEqualsAndHashCodeContract(a, a, true, "bool2 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"bool2 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 2, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "bool2 =="); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void Bool3EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new bool3(true, false, true); + var b = a; + b[index] = !b[index]; + + AssertEqualsAndHashCodeContract(a, a, true, "bool3 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"bool3 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 3, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "bool3 =="); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void Bool4EqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new bool4(true, false, true, false); + var b = a; + b[index] = !b[index]; + + AssertEqualsAndHashCodeContract(a, a, true, "bool4 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"bool4 differing at {index}"); + AssertElementwiseEqualityOperators( + a, b, 4, i => i != index, (x, y) => x == y, (x, y) => x != y, (v, i) => v[i], "bool4 =="); + } + + // ---- quaternion (== and != return a plain bool, not a component vector) ----------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void QuaternionEqualsHashCodeAndOperatorsDetectSingleComponentDifference(int index) + { + var a = new quaternion(1.0f, 2.0f, 3.0f, 4.0f); + float4 asFloat4 = a; + asFloat4[index] += 5.0f; + quaternion b = asFloat4; + + AssertEqualsAndHashCodeContract(a, a, true, "quaternion self"); + AssertEqualsAndHashCodeContract(a, b, false, $"quaternion differing at {index}"); + + // quaternion's == / != are scalar bool, defined in terms of Equals: confirm they + // actually delegate rather than reimplementing (and weakening) the comparison. + var aCopy = new quaternion(a.x, a.y, a.z, a.w); + Assert.Equal(a.Equals(aCopy), a == aCopy); + Assert.Equal(!a.Equals(aCopy), a != aCopy); + Assert.Equal(a.Equals(b), a == b); + Assert.Equal(!a.Equals(b), a != b); + } + + // ---- float2x2 / float3x3 (Equals/GetHashCode only, row granularity) --------------------- + + [Theory] + [InlineData(0)] + [InlineData(1)] + public void Float2x2EqualsAndHashCodeDetectSingleRowDifference(int row) + { + var row0 = new float2(1.0f, 2.0f); + var row1 = new float2(3.0f, 4.0f); + var a = new float2x2(row0, row1); + + var differingRow0 = row == 0 ? row0 + new float2(10.0f, 0.0f) : row0; + var differingRow1 = row == 1 ? row1 + new float2(0.0f, 10.0f) : row1; + var b = new float2x2(differingRow0, differingRow1); + + AssertEqualsAndHashCodeContract(a, a, true, "float2x2 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"float2x2 differing at row {row}"); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void Float3x3EqualsAndHashCodeDetectSingleRowDifference(int row) + { + var row0 = new float3(1.0f, 2.0f, 3.0f); + var row1 = new float3(4.0f, 5.0f, 6.0f); + var row2 = new float3(7.0f, 8.0f, 9.0f); + var a = new float3x3(row0, row1, row2); + + var differingRow0 = row == 0 ? row0 + new float3(10.0f, 0.0f, 0.0f) : row0; + var differingRow1 = row == 1 ? row1 + new float3(0.0f, 10.0f, 0.0f) : row1; + var differingRow2 = row == 2 ? row2 + new float3(0.0f, 0.0f, 10.0f) : row2; + var b = new float3x3(differingRow0, differingRow1, differingRow2); + + AssertEqualsAndHashCodeContract(a, a, true, "float3x3 self"); + AssertEqualsAndHashCodeContract(a, b, false, $"float3x3 differing at row {row}"); + } +}