From 06a866b48c572ab292b05259ac68cd24ad979fc8 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 23:07:59 -0400 Subject: [PATCH 1/7] llmath's vector, colour and rect tests check what they say they check Bugs in the tests themselves, kept through the conversion to Catch2 and fixed here: - The subscript cases (v3math, v3dmath, v4math, v4color) compared each element with itself through a reference to it, which is always true. They now check that the non-const subscript is a reference to the component, and that writing through it writes the component. - The LLSD round trips (v3math, v3dmath) built the vector from a scalar LLSD, but a vector reads sd[0], sd[1] and sd[2], so every component was zero and the round trip compared zeros. They use arrays of three and check the components. v3math's assignment case had the same zero vector. - The rotVec cases (v3math, v4math) compared two identical computations. They now check a quarter turn exactly, any angle about any axis against Rodrigues' formula in double, the matrix product against its sum, and that the quaternion turn leaves w alone. - v3math's operator< case ran its last comparison through a trailing comma and only ever compared x; it now checks x, then y, then z, and strictness. Its operator!= case cleared one vector twice. - v3dmath set F64 values from float literals where nothing needed them; the one place that does (a trip through LLVector3) says why. - v3color's += and -= cases repeated their first halves exactly; the second halves use other values and check the returned reference. - llrect checked (-0.0001, -0.001) where it meant (-0.0001, -0.0001), and compared a rect only with itself; it now checks each side and each edge. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/tests/llrect_test.cpp | 26 +++++- indra/llmath/tests/v3color_test.cpp | 12 ++- indra/llmath/tests/v3dmath_test.cpp | 63 +++++++++------ indra/llmath/tests/v3math_test.cpp | 118 ++++++++++++++++++---------- indra/llmath/tests/v4color_test.cpp | 15 ++-- indra/llmath/tests/v4math_test.cpp | 63 ++++++++++----- 6 files changed, 202 insertions(+), 95 deletions(-) diff --git a/indra/llmath/tests/llrect_test.cpp b/indra/llmath/tests/llrect_test.cpp index 72a8e22f5d..d4cd29e487 100644 --- a/indra/llmath/tests/llrect_test.cpp +++ b/indra/llmath/tests/llrect_test.cpp @@ -311,8 +311,25 @@ namespace CHECK_FALSE(rect1 == rect2); CHECK(rect1 != rect2); - CHECK(rect1 == rect1); - CHECK_FALSE(rect1 != rect1); + // Another rect with the same edges, not the same rect. + LLRectf same; + same.set(-1.0f, 1.0f, 1.0f, -1.0f); + CHECK(rect1 == same); + CHECK_FALSE(rect1 != same); + + // Each edge counts. + const LLRectf apart[] = { + LLRectf(-1.5f, 1.0f, 1.0f, -1.0f), + LLRectf(-1.0f, 1.5f, 1.0f, -1.0f), + LLRectf(-1.0f, 1.0f, 1.5f, -1.0f), + LLRectf(-1.0f, 1.0f, 1.0f, -1.5f), + }; + for (const LLRectf& other : apart) + { + CAPTURE(other); + CHECK(rect1 != other); + CHECK_FALSE(rect1 == other); + } } TEST_CASE("LLRect: copy constructor gives an equal rect", "[LLRect]") @@ -478,7 +495,10 @@ namespace LLRectf rect(1.0f, 3.0f, 3.0f, 1.0f); CHECK(rect.localPointInRect(0.0f, 0.0f)); - CHECK_FALSE(rect.localPointInRect(-0.0001f, -0.001f)); + CHECK_FALSE(rect.localPointInRect(-0.0001f, -0.0001f)); + // Just outside on one side is outside. + CHECK_FALSE(rect.localPointInRect(-0.0001f, 0.0f)); + CHECK_FALSE(rect.localPointInRect(0.0f, -0.0001f)); CHECK(rect.localPointInRect(1.0f, 1.0f)); CHECK_FALSE(rect.localPointInRect(2.0f, 2.0f)); CHECK(rect.localPointInRect(1.999f, 1.999f)); diff --git a/indra/llmath/tests/v3color_test.cpp b/indra/llmath/tests/v3color_test.cpp index 7ffa6cb49a..906f3a4bd9 100644 --- a/indra/llmath/tests/v3color_test.cpp +++ b/indra/llmath/tests/v3color_test.cpp @@ -236,9 +236,13 @@ namespace CHECK(is_approx_equal(r1+r2 ,llcolor3a.mV[0])); CHECK(is_approx_equal(g1+g2,llcolor3a.mV[1])); CHECK(is_approx_equal(b1+b2,llcolor3a.mV[2])); + + // Again on other values, and it hands back the color it changed. + r1 = -.235f, g1 = -24.32f, b1 = 2.13f, r2 = .5f, g2 = 1.f, b2 = -34.21f; llcolor3.setVec(r1,g1,b1); llcolor3a.setVec(r2,g2,b2); - llcolor3a += llcolor3; + const LLColor3& sum = (llcolor3a += llcolor3); + CHECK(&sum == &llcolor3a); CHECK(is_approx_equal(r1+r2 ,llcolor3a.mV[0])); CHECK(is_approx_equal(g1+g2,llcolor3a.mV[1])); CHECK(is_approx_equal(b1+b2,llcolor3a.mV[2])); @@ -252,9 +256,13 @@ namespace CHECK(is_approx_equal(r2-r1, llcolor3a.mV[0])); CHECK(is_approx_equal(g2-g1, llcolor3a.mV[1])); CHECK(is_approx_equal(b2-b1, llcolor3a.mV[2])); + + // Again on other values, and it hands back the color it changed. + r1 = -.235f, g1 = -24.32f, b1 = 2.13f, r2 = .5f, g2 = 1.f, b2 = -34.21f; llcolor3.setVec(r1,g1,b1); llcolor3a.setVec(r2,g2,b2); - llcolor3a -= llcolor3; + const LLColor3& difference = (llcolor3a -= llcolor3); + CHECK(&difference == &llcolor3a); CHECK(is_approx_equal(r2-r1, llcolor3a.mV[0])); CHECK(is_approx_equal(g2-g1, llcolor3a.mV[1])); CHECK(is_approx_equal(b2-b1, llcolor3a.mV[2])); diff --git a/indra/llmath/tests/v3dmath_test.cpp b/indra/llmath/tests/v3dmath_test.cpp index 3f660402ec..58f62a29a8 100644 --- a/indra/llmath/tests/v3dmath_test.cpp +++ b/indra/llmath/tests/v3dmath_test.cpp @@ -46,28 +46,38 @@ namespace CHECK(vec3D.mdV[VY] == 0); CHECK(vec3D.mdV[VZ] == 0); - F64 x = 2.32f, y = 1.212f, z = -.12f; + F64 x = 2.32, y = 1.212, z = -.12; LLVector3d vec3Da(x,y,z); - CHECK(vec3Da.mdV[VX] == 2.32f); - CHECK(vec3Da.mdV[VY] == 1.212f); - CHECK(vec3Da.mdV[VZ] == -.12f); + CHECK(vec3Da.mdV[VX] == 2.32); + CHECK(vec3Da.mdV[VY] == 1.212); + CHECK(vec3Da.mdV[VZ] == -.12); - const F64 vec[3] = {1.2f ,3.2f, -4.2f}; + const F64 vec[3] = {1.2 ,3.2, -4.2}; LLVector3d vec3Db(vec); - CHECK(vec3Db.mdV[VX] == 1.2f); - CHECK(vec3Db.mdV[VY] == 3.2f); - CHECK(vec3Db.mdV[VZ] == -4.2f); + CHECK(vec3Db.mdV[VX] == 1.2); + CHECK(vec3Db.mdV[VY] == 3.2); + CHECK(vec3Db.mdV[VZ] == -4.2); - LLVector3 vec3((F32)x,(F32)y,(F32)z); + // From an LLVector3, each float widened exactly. + LLVector3 vec3(2.32f, 1.212f, -.12f); LLVector3d vec3Dc(vec3); - CHECK(vec3Da == vec3Dc); + CHECK(vec3Dc.mdV[VX] == F64(2.32f)); + CHECK(vec3Dc.mdV[VY] == F64(1.212f)); + CHECK(vec3Dc.mdV[VZ] == F64(-.12f)); } TEST_CASE("v3dmath_h: LLSD constructor and getValue round trip", "[v3dmath_h]") { - S32 a = -235; - LLSD llsd(a); + // An array of three: a vector reads sd[0], sd[1] and sd[2], so a + // scalar LLSD would make every component zero. + LLSD llsd; + llsd[0] = -235.0; + llsd[1] = 232345521.411132; + llsd[2] = -.12; LLVector3d vec3d(llsd); + CHECK(vec3d.mdV[VX] == -235.0); + CHECK(vec3d.mdV[VY] == 232345521.411132); + CHECK(vec3d.mdV[VZ] == -.12); LLSD sd = vec3d.getValue(); LLVector3d vec3da(sd); CHECK(vec3d == vec3da); @@ -75,12 +85,18 @@ namespace TEST_CASE("v3dmath_h: setValue and getValue round trip", "[v3dmath_h]") { - F64 a = 232345521.411132; - LLSD llsd(a); + LLSD llsd; + llsd[0] = 232345521.411132; + llsd[1] = -0.000001; + llsd[2] = 12345.2343; LLVector3d vec3d; vec3d.setValue(llsd); + CHECK(vec3d.mdV[VX] == 232345521.411132); + CHECK(vec3d.mdV[VY] == -0.000001); + CHECK(vec3d.mdV[VZ] == 12345.2343); LLSD sd = vec3d.getValue(); - LLVector3d vec3da(sd); + LLVector3d vec3da; + vec3da.setValue(sd); CHECK(vec3d == vec3da); } @@ -100,6 +116,7 @@ namespace TEST_CASE("v3dmath_h: clearVec/zeroVec and the setVec overloads", "[v3dmath_h]") { + // Values a float holds, so the trip through an LLVector3 below is exact. F64 x = 2.32f, y = 1.212f, z = -.12f; LLVector3d vec3D(x,y,z); vec3D.clearVec(); @@ -164,15 +181,15 @@ namespace CHECK(x == vec3D[0]); CHECK(y == vec3D[1]); CHECK(z == vec3D[2]); - vec3D.clearVec(); - x = 23.23, y = -.2361, z = 3.25; - vec3D.setVec(x,y,z); + // The non-const subscript is a reference to the component itself. F64 &ref1 = vec3D[0]; - CHECK(ref1 == vec3D[0]); - F64 &ref2 = vec3D[1]; - CHECK(ref2 == vec3D[1]); - F64 &ref3 = vec3D[2]; - CHECK(ref3 == vec3D[2]); + CHECK(&ref1 == &vec3D.mdV[VX]); + ref1 = 23.23; + vec3D[1] = -.2361; + vec3D[2] = 3.25; + CHECK(vec3D.mdV[VX] == 23.23); + CHECK(vec3D.mdV[VY] == -.2361); + CHECK(vec3D.mdV[VZ] == 3.25); } TEST_CASE("v3dmath_h: assignment from LLVector4", "[v3dmath_h]") diff --git a/indra/llmath/tests/v3math_test.cpp b/indra/llmath/tests/v3math_test.cpp index 18ded63c88..b5368bdc0e 100644 --- a/indra/llmath/tests/v3math_test.cpp +++ b/indra/llmath/tests/v3math_test.cpp @@ -28,6 +28,7 @@ #include "linden_common.h" #include +#include #include "llsd.h" #include "../v3dmath.h" @@ -74,9 +75,16 @@ namespace TEST_CASE("v3math_h: LLSD constructor and getValue round trip", "[v3math_h]") { - S32 a = 231; - LLSD llsd(a); + // An array of three: a vector reads sd[0], sd[1] and sd[2], so a + // scalar LLSD would make every component zero. + LLSD llsd; + llsd[0] = 2.32; + llsd[1] = 1.212; + llsd[2] = -.12; LLVector3 vec3(llsd); + CHECK(vec3.mV[VX] == 2.32f); + CHECK(vec3.mV[VY] == 1.212f); + CHECK(vec3.mV[VZ] == -.12f); LLSD sd = vec3.getValue(); LLVector3 vec3a(sd); CHECK(vec3 == vec3a); @@ -84,11 +92,11 @@ namespace TEST_CASE("v3math_h: assignment copies every component", "[v3math_h]") { - S32 a = 231; - LLSD llsd(a); - LLVector3 vec3(llsd),vec3a; + LLVector3 vec3(2.32f, 1.212f, -.12f), vec3a; vec3a = vec3; - CHECK(vec3 == vec3a); + CHECK(vec3a.mV[VX] == 2.32f); + CHECK(vec3a.mV[VY] == 1.212f); + CHECK(vec3a.mV[VZ] == -.12f); } TEST_CASE("v3math_h: isFinite and the clearVec/setVec/zeroVec setters", "[v3math_h]") @@ -193,15 +201,43 @@ namespace CHECK_FALSE(vec3.isExactlyZero()); } - TEST_CASE("v3math_h: rotVec by an angle about an axis is repeatable", "[v3math_h]") - { - F32 x =20.0f, y = 30.0f, z = 15.f ; - F32 angle = 100.f; - LLVector3 vec3(x,y,z),vec3a(1.f,2.f,3.f); - vec3a = vec3a.rotVec(angle, vec3); - LLVector3 vec3b(1.f,2.f,3.f); - vec3b = vec3b.rotVec(angle, vec3); - CHECK(vec3b == vec3a); + TEST_CASE("v3math_h: rotVec turns a vector by an angle about an axis", "[v3math_h]") + { + using Catch::Matchers::WithinAbs; + + // A quarter turn about z takes x to y. + LLVector3 quarter(1.f, 0.f, 0.f); + quarter.rotVec(F_PI_BY_TWO, LLVector3(0.f, 0.f, 1.f)); + CHECK_THAT(quarter.mV[VX], WithinAbs(0.f, 1e-6)); + CHECK_THAT(quarter.mV[VY], WithinAbs(1.f, 1e-6)); + CHECK_THAT(quarter.mV[VZ], WithinAbs(0.f, 1e-6)); + + // Any angle about any axis, against Rodrigues' formula in double: + // v cos a + (k x v) sin a + k (k . v)(1 - cos a), k the unit axis. + const F64 angle = 100.0; + const F64 axis_length = sqrt(20.0 * 20.0 + 30.0 * 30.0 + 15.0 * 15.0); + const F64 k[3] = { 20.0 / axis_length, 30.0 / axis_length, 15.0 / axis_length }; + const F64 v[3] = { 1.0, 2.0, 3.0 }; + const F64 k_dot_v = k[0] * v[0] + k[1] * v[1] + k[2] * v[2]; + const F64 k_cross_v[3] = { k[1] * v[2] - k[2] * v[1], k[2] * v[0] - k[0] * v[2], k[0] * v[1] - k[1] * v[0] }; + F64 expected[3]; + for (int i = 0; i < 3; ++i) + { + expected[i] = v[i] * cos(angle) + k_cross_v[i] * sin(angle) + k[i] * k_dot_v * (1.0 - cos(angle)); + } + + LLVector3 vec3((F32)v[0], (F32)v[1], (F32)v[2]); + vec3.rotVec((F32)angle, LLVector3(20.f, 30.f, 15.f)); + CHECK_THAT(vec3.mV[VX], WithinAbs(expected[0], 1e-4)); + CHECK_THAT(vec3.mV[VY], WithinAbs(expected[1], 1e-4)); + CHECK_THAT(vec3.mV[VZ], WithinAbs(expected[2], 1e-4)); + + // No turn about a zero axis, or by a zero angle. + LLVector3 still(1.f, 2.f, 3.f); + still.rotVec((F32)angle, LLVector3::zero); + CHECK(still == LLVector3(1.f, 2.f, 3.f)); + still.rotVec(0.f, LLVector3(20.f, 30.f, 15.f)); + CHECK(still == LLVector3(1.f, 2.f, 3.f)); } TEST_CASE("v3math_h: subscript operator reads components and returns references", "[v3math_h]") @@ -212,15 +248,15 @@ namespace CHECK(y == vec3[1]); CHECK(z == vec3[2]); - vec3.clearVec(); - x = 23.f, y = -.2361f, z = 3.25; - vec3.setVec(x,y,z); + // The non-const subscript is a reference to the component itself. F32 &ref1 = vec3[0]; - CHECK(ref1 == vec3[0]); - F32 &ref2 = vec3[1]; - CHECK(ref2 == vec3[1]); - F32 &ref3 = vec3[2]; - CHECK(ref3 == vec3[2]); + CHECK(&ref1 == &vec3.mV[VX]); + ref1 = 23.f; + vec3[1] = -.2361f; + vec3[2] = 3.25f; + CHECK(vec3.mV[VX] == 23.f); + CHECK(vec3.mV[VY] == -.2361f); + CHECK(vec3.mV[VZ] == 3.25f); } TEST_CASE("v3math_h: operator+ adds componentwise", "[v3math_h]") @@ -376,19 +412,21 @@ namespace TEST_CASE("v3math_h: operator!= for different and equal vectors", "[v3math_h]") { - F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 =112.f, y2 = 2.234f,z2 = 11.2f;; + F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 =112.f, y2 = 2.234f,z2 = 11.2f; LLVector3 vec3(x1,y1,z1), vec3a(x2,y2,z2); CHECK(vec3a != vec3); - vec3.clearVec(); - vec3.clearVec(); + // One component apart is enough. + vec3a.setVec(x1, y1, z2); + CHECK(vec3a != vec3); + vec3a.setVec(vec3); CHECK_FALSE(vec3a != vec3); } TEST_CASE("v3math_h: operator+= adds componentwise", "[v3math_h]") { - F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 =112.f, y2 = 2.2f,z2 = 11.2f;; + F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 =112.f, y2 = 2.2f,z2 = 11.2f; LLVector3 vec3(x1,y1,z1), vec3a(x2,y2,z2); vec3a += vec3; F32 val1, val2, val3; @@ -402,7 +440,7 @@ namespace TEST_CASE("v3math_h: operator-= subtracts componentwise", "[v3math_h]") { - F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 =112.f, y2 = 2.2f,z2 = 11.2f;; + F32 x1 =1.f, y1 = 2.f,z1 = 1.2f, x2 =112.f, y2 = 2.2f,z2 = 11.2f; LLVector3 vec3(x1,y1,z1), vec3a(x2,y2,z2); vec3a -= vec3; F32 val1, val2, val3; @@ -479,19 +517,19 @@ namespace CHECK(stream1.str() == stream2.str()); } - TEST_CASE("v3math_h: operator< orders vectors by x first", "[v3math_h]") + TEST_CASE("v3math_h: operator< orders by x, then y, then z", "[v3math_h]") { - F32 x1 =-2.3f, y1 = 2.f,z1 = 1.2f, x2 = 1.3f, y2 = 1.11f, z2 = 1234.234f; - LLVector3 vec3(x1,y1,z1), vec3a(x2,y2,z2); - CHECK(vec3 < vec3a); - x1 =-2.3f, y1 = 2.f,z1 = 1.2f, x2 = 1.3f, y2 = 2.f, z2 = 1234.234f; - vec3.setVec(x1,y1,z1); - vec3a.setVec(x2,y2,z2); - CHECK(vec3 < vec3a); - x1 =2.3f, y1 = 2.f,z1 = 1.2f, x2 = 1.3f, - vec3.setVec(x1,y1,z1); - vec3a.setVec(x2,y2,z2); - CHECK_FALSE(vec3 < vec3a); + // x decides wherever the two differ in it, whatever y and z say. + CHECK(LLVector3(-2.3f, 2.f, 1.2f) < LLVector3(1.3f, 1.11f, 1234.234f)); + CHECK_FALSE(LLVector3(2.3f, 2.f, 1.2f) < LLVector3(1.3f, 2.f, 1234.234f)); + // Equal in x, y decides. + CHECK(LLVector3(1.f, 2.f, 9.f) < LLVector3(1.f, 3.f, 0.f)); + CHECK_FALSE(LLVector3(1.f, 3.f, 0.f) < LLVector3(1.f, 2.f, 9.f)); + // Equal in x and y, z decides. + CHECK(LLVector3(1.f, 2.f, 3.f) < LLVector3(1.f, 2.f, 4.f)); + CHECK_FALSE(LLVector3(1.f, 2.f, 4.f) < LLVector3(1.f, 2.f, 3.f)); + // Strict: no vector is less than itself. + CHECK_FALSE(LLVector3(1.f, 2.f, 3.f) < LLVector3(1.f, 2.f, 3.f)); } TEST_CASE("v3math_h: parseVector3 reads three numbers", "[v3math_h]") diff --git a/indra/llmath/tests/v4color_test.cpp b/indra/llmath/tests/v4color_test.cpp index 3f72e62c75..759f396ef1 100644 --- a/indra/llmath/tests/v4color_test.cpp +++ b/indra/llmath/tests/v4color_test.cpp @@ -218,14 +218,15 @@ namespace CHECK(g == llcolor4[1]); CHECK(b == llcolor4[2]); - r = 0xA20, g = 0xFBFF, b = 0xFFF; - llcolor4.setVec(r,g,b); + // The non-const subscript is a reference to the component itself. F32 &ref1 = llcolor4[0]; - CHECK(ref1 == llcolor4[0]); - F32 &ref2 = llcolor4[1]; - CHECK(ref2 == llcolor4[1]); - F32 &ref3 = llcolor4[2]; - CHECK(ref3 == llcolor4[2]); + CHECK(&ref1 == &llcolor4.mV[VRED]); + ref1 = 0xA20; + llcolor4[1] = 0xFBFF; + llcolor4[2] = 0xFFF; + CHECK(llcolor4.mV[VRED] == 0xA20); + CHECK(llcolor4.mV[VGREEN] == 0xFBFF); + CHECK(llcolor4.mV[VBLUE] == 0xFFF); } TEST_CASE("v4color_h: assignment from LLColor3 and construction from LLSD", "[v4color_h]") diff --git a/indra/llmath/tests/v4math_test.cpp b/indra/llmath/tests/v4math_test.cpp index ac96ff8515..80e93d4d4a 100644 --- a/indra/llmath/tests/v4math_test.cpp +++ b/indra/llmath/tests/v4math_test.cpp @@ -28,6 +28,7 @@ #include "linden_common.h" #include +#include #include "altestmatchers.h" #include "llsd.h" @@ -182,20 +183,24 @@ namespace CHECK(y == vec4[1]); CHECK(z == vec4[2]); CHECK(w == vec4[3]); - x = 23.f, y = -.2361f, z = 3.25; - vec4.setVec(x,y,z); + // The non-const subscript is a reference to the component itself. F32 &ref1 = vec4[0]; - CHECK(ref1 == vec4[0]); - F32 &ref2 = vec4[1]; - CHECK(ref2 == vec4[1]); - F32 &ref3 = vec4[2]; - CHECK(ref3 == vec4[2]); - F32 &ref4 = vec4[3]; - CHECK(ref4 == vec4[3]); + CHECK(&ref1 == &vec4.mV[VX]); + ref1 = 23.f; + vec4[1] = -.2361f; + vec4[2] = 3.25f; + vec4[3] = 7.5f; + CHECK(vec4.mV[VX] == 23.f); + CHECK(vec4.mV[VY] == -.2361f); + CHECK(vec4.mV[VZ] == 3.25f); + CHECK(vec4.mV[VW] == 7.5f); } - TEST_CASE("v4math_h: rotVec by a matrix and by a quaternion is repeatable", "[v4math_h]") + TEST_CASE("v4math_h: rotVec by a matrix and by a quaternion", "[v4math_h]") { + using Catch::Matchers::WithinAbs; + + // By a matrix: the row vector times the matrix, all four components. F32 x = 10.f, y = -2.3f, z = -.023f, w = -2.0f; const F32 val[16] = { 1.f, 2.f, 3.f, 0.f, @@ -204,18 +209,36 @@ namespace .89f, 0.f, 0.f, 0.f }; LLMatrix4 mat(val); - LLVector4 vec4(x,y,z,w),vec4a; + LLVector4 vec4(x,y,z,w); vec4.rotVec(mat); - vec4a.setVec(x,y,z,w); - vec4a.rotVec(mat); - CHECK(vec4a == vec4); - F32 a = 2.32f, b = -23.2f, c = -34.1112f, d = 1.010112f; - LLQuaternion q(a,b,c,d); - LLVector4 vec4b(a,b,c,d),vec4c; + const F64 v[4] = { x, y, z, w }; + for (int col = 0; col < 4; ++col) + { + F64 expected = 0.0; + for (int row = 0; row < 4; ++row) + { + expected += v[row] * val[row * 4 + col]; + } + CAPTURE(col); + CHECK_THAT(vec4.mV[col], WithinAbs(expected, 1e-4)); + } + + // By a quaternion: x, y and z turn as an LLVector3 does, and w stays. + LLVector4 quarter(1.f, 0.f, 0.f, 3.f); + quarter.rotVec(LLQuaternion(F_PI_BY_TWO, LLVector3(0.f, 0.f, 1.f))); + CHECK_THAT(quarter.mV[VX], WithinAbs(0.f, 1e-6)); + CHECK_THAT(quarter.mV[VY], WithinAbs(1.f, 1e-6)); + CHECK_THAT(quarter.mV[VZ], WithinAbs(0.f, 1e-6)); + CHECK(quarter.mV[VW] == 3.f); + + const LLQuaternion q(1.3f, LLVector3(2.32f, -23.2f, -34.1112f)); + LLVector4 vec4b(2.32f, -23.2f, -34.1112f, 1.010112f); vec4b.rotVec(q); - vec4c.setVec(a, b, c, d); - vec4c.rotVec(q); - CHECK(vec4b == vec4c); + const LLVector3 turned = LLVector3(2.32f, -23.2f, -34.1112f) * q; + CHECK(vec4b.mV[VX] == turned.mV[VX]); + CHECK(vec4b.mV[VY] == turned.mV[VY]); + CHECK(vec4b.mV[VZ] == turned.mV[VZ]); + CHECK(vec4b.mV[VW] == 1.010112f); } TEST_CASE("v4math_h: operator<< prints equal vectors the same", "[v4math_h]") From 01bf66bbe0dc67af179ed26ff34ab22e327afb29 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 23:07:59 -0400 Subject: [PATCH 2/7] xform asks its change flags one at a time, and llbbox tests the rotated extent and addBBoxAgent - isChanged(bits) is true when any of the bits is set, so xform's isChanged(TRANSLATED | SCALED) after a setPosition could not tell whether setPosition kept SCALED, nor isChanged(TRANSLATED | ROTATED) whether setRotation set ROTATED. Each flag is asked on its own, with the flags that must stay clear. getMinMax's case checked the minimum corner twice and never the maximum, on vectors already zero; both corners are checked, starting from ones. - llbbox's "with rotation" extent check tested the unrotated box again. Its "addBBoxAgent" case called addBBoxLocal, so addBBoxAgent was never tested: two cases now do, into an empty box and into one at the origin. - llbboxlocal built a box with min above max and never looked at it; its extent is checked. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/tests/llbbox_test.cpp | 38 +++++++++++++++++++++---- indra/llmath/tests/llbboxlocal_test.cpp | 8 ++++-- indra/llmath/tests/xform_test.cpp | 35 ++++++++++++++++------- 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/indra/llmath/tests/llbbox_test.cpp b/indra/llmath/tests/llbbox_test.cpp index 726bf6d8ff..e88e98b2cf 100644 --- a/indra/llmath/tests/llbbox_test.cpp +++ b/indra/llmath/tests/llbbox_test.cpp @@ -144,10 +144,11 @@ namespace CHECK(bbox1.getExtentLocal() == LLVector3(2.0f, 2.0f, 2.0f)); + // Local: the rotation and position do not enter into it. LLBBox bbox2(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(ANGLE, LLVector3(0.0f, 0.0f, 1.0f)), - LLVector3(2.0f, 2.0f, 2.0f), LLVector3(4.0f, 4.0f, 4.0f)); + LLVector3(2.0f, 2.0f, 2.0f), LLVector3(4.0f, 5.0f, 6.0f)); - CHECK(bbox1.getExtentLocal() == LLVector3(2.0f, 2.0f, 2.0f)); + CHECK(bbox2.getExtentLocal() == LLVector3(2.0f, 3.0f, 4.0f)); } TEST_CASE("LLBBox: addPointLocal grows the box", "[LLBBox]") @@ -217,9 +218,6 @@ namespace TEST_CASE("LLBBox: addBBoxLocal after addPointAgent on a rotated box", "[LLBBox]") { - // - // test the addBBoxAgent() method - // LLBBox bbox1(LLVector3(1.0f, 1.0f, 1.0f), LLQuaternion(1.0, 0.0, 0.0, 1.0), LLVector3(2.0f, 2.0f, 2.0f), LLVector3(4.0f, 4.0f, 4.0f)); @@ -236,6 +234,36 @@ namespace CHECK(bbox1.getMaxLocal() == LLVector3(10.0f, 10.0f, 10.0f)); } + TEST_CASE("LLBBox: addBBoxAgent into an empty box takes the other box's frame", "[LLBBox]") + { + const LLQuaternion quarter(ANGLE, LLVector3(0.0f, 0.0f, 1.0f)); + LLBBox bbox1; + bbox1.addBBoxAgent(LLBBox(LLVector3(1.0f, 2.0f, 3.0f), quarter, + LLVector3(0.0f, 0.0f, 0.0f), LLVector3(2.0f, 4.0f, 6.0f))); + + CHECK(bbox1.getPositionAgent() == LLVector3(1.0f, 2.0f, 3.0f)); + CHECK(bbox1.getRotation() == quarter); + CHECK(APPROX_EQUAL(bbox1.getMinLocal(), LLVector3(0.0f, 0.0f, 0.0f))); + CHECK(APPROX_EQUAL(bbox1.getMaxLocal(), LLVector3(2.0f, 4.0f, 6.0f))); + } + + TEST_CASE("LLBBox: addBBoxAgent grows a box by the other box's corners in agent space", "[LLBBox]") + { + // An axis-aligned box at the origin, holding only the origin. + LLBBox bbox1; + bbox1.addPointLocal(LLVector3(0.0f, 0.0f, 0.0f)); + + // Two by one by one, turned a quarter about z, at (10, 0, 0): x goes + // to y and y to -x, so its corners span x 9 to 10, y 0 to 2, z 0 to 1. + bbox1.addBBoxAgent(LLBBox(LLVector3(10.0f, 0.0f, 0.0f), LLQuaternion(ANGLE, LLVector3(0.0f, 0.0f, 1.0f)), + LLVector3(0.0f, 0.0f, 0.0f), LLVector3(2.0f, 1.0f, 1.0f))); + + CHECK(bbox1.getPositionAgent() == LLVector3(0.0f, 0.0f, 0.0f)); + CHECK(bbox1.getRotation() == LLQuaternion()); + CHECK(APPROX_EQUAL(bbox1.getMinLocal(), LLVector3(0.0f, 0.0f, 0.0f))); + CHECK(APPROX_EQUAL(bbox1.getMaxLocal(), LLVector3(10.0f, 2.0f, 1.0f))); + } + TEST_CASE("LLBBox: expand by zero then a positive then a negative amount", "[LLBBox]") { // diff --git a/indra/llmath/tests/llbboxlocal_test.cpp b/indra/llmath/tests/llbboxlocal_test.cpp index df9694e905..d29df8ed58 100644 --- a/indra/llmath/tests/llbboxlocal_test.cpp +++ b/indra/llmath/tests/llbboxlocal_test.cpp @@ -114,13 +114,17 @@ namespace // test the getExtent() method // - LLBBoxLocal bbox2(LLVector3(0.0f, 0.0f, 0.0f), LLVector3(-1.0f, -1.0f, -1.0f)); - CHECK(LLBBoxLocal().getExtent() == LLVector3(0.0f, 0.0f, 0.0f)); LLBBoxLocal bbox3(LLVector3(-1.0f, -1.0f, -1.0f), LLVector3(1.0f, 2.0f, 0.0f)); CHECK(bbox3.getExtent() == LLVector3(2.0f, 3.0f, 1.0f)); + + // Max less min, so negative where min is above max. + LLBBoxLocal bbox2(LLVector3(0.0f, 0.0f, 0.0f), LLVector3(-1.0f, -1.0f, -1.0f)); + + UNSCOPED_INFO("min above max"); + CHECK(bbox2.getExtent() == LLVector3(-1.0f, -1.0f, -1.0f)); } TEST_CASE("LLBBoxLocal: addPoint grows the box only for points outside it", "[LLBBoxLocal]") diff --git a/indra/llmath/tests/xform_test.cpp b/indra/llmath/tests/xform_test.cpp index bfda263550..ff78bf24ef 100644 --- a/indra/llmath/tests/xform_test.cpp +++ b/indra/llmath/tests/xform_test.cpp @@ -140,20 +140,34 @@ namespace xform_obj.clearChanged(LLXform::TRANSLATED | LLXform::ROTATED | LLXform::SCALED); CHECK_FALSE(xform_obj.isChanged()); + // isChanged(bits) is true when any of the bits is set, so each flag + // is asked about on its own. LLVector3 llvect3(12.4f, -5.6f, 0.34f); xform_obj.setScale(llvect3); CHECK(xform_obj.isChanged(LLXform::SCALED)); + CHECK_FALSE(xform_obj.isChanged(LLXform::TRANSLATED)); + CHECK_FALSE(xform_obj.isChanged(LLXform::ROTATED)); + xform_obj.setPosition(1.2f, 2.3f, 3.4f); CHECK(xform_obj.isChanged(LLXform::TRANSLATED)); UNSCOPED_INFO("setPosition must not clear SCALED"); - CHECK(xform_obj.isChanged(LLXform::TRANSLATED | LLXform::SCALED)); + CHECK(xform_obj.isChanged(LLXform::SCALED)); + CHECK_FALSE(xform_obj.isChanged(LLXform::ROTATED)); + xform_obj.clearChanged(LLXform::SCALED); CHECK_FALSE(xform_obj.isChanged(LLXform::SCALED)); + UNSCOPED_INFO("clearing SCALED must leave TRANSLATED"); + CHECK(xform_obj.isChanged(LLXform::TRANSLATED)); + xform_obj.setRotation(1, 2, 3, 4); - UNSCOPED_INFO("setRotation must set ROTATED"); - CHECK(xform_obj.isChanged(LLXform::TRANSLATED | LLXform::ROTATED)); + CHECK(xform_obj.isChanged(LLXform::ROTATED)); + CHECK_FALSE(xform_obj.isChanged(LLXform::SCALED)); + + // MOVED is the three together. xform_obj.setScale(llvect3); - CHECK(xform_obj.isChanged(LLXform::MOVED)); + CHECK(xform_obj.isChanged(LLXform::SCALED)); + xform_obj.clearChanged(LLXform::MOVED); + CHECK_FALSE(xform_obj.isChanged()); } //to test init() and getWorldMatrix() fns. @@ -182,19 +196,20 @@ namespace } //to test mMin.clearVec() and mMax.clearVec() fns - TEST_CASE("LLXForm: getMinMax after init puts the minimum corner at the origin", "[LLXForm]") + TEST_CASE("LLXForm: getMinMax after init puts both corners at the origin", "[LLXForm]") { LLXformMatrix formMatrix_obj; formMatrix_obj.init(); - LLVector3 llmin_vec3; - LLVector3 llmax_vec3; + // Not zero to begin with, so the zeros below are getMinMax's. + LLVector3 llmin_vec3(1.f, 1.f, 1.f); + LLVector3 llmax_vec3(1.f, 1.f, 1.f); formMatrix_obj.getMinMax(llmin_vec3, llmax_vec3); CHECK(llmin_vec3.mV[0] == 0.f); CHECK(llmin_vec3.mV[1] == 0.f); CHECK(llmin_vec3.mV[2] == 0.f); - CHECK(llmin_vec3.mV[0] == 0.f); - CHECK(llmin_vec3.mV[1] == 0.f); - CHECK(llmin_vec3.mV[2] == 0.f); + CHECK(llmax_vec3.mV[0] == 0.f); + CHECK(llmax_vec3.mV[1] == 0.f); + CHECK(llmax_vec3.mV[2] == 0.f); } //test case of update() fn. From 4712efa15a14e9be152ee31964d5ffc5538e2245 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 23:07:59 -0400 Subject: [PATCH 3/7] llquaternion's tests build quaternions from matrices and read Euler angles back The cases commented as tests of LLQuaternion(const LLMatrix4&) and LLQuaternion(const LLMatrix3&) only filled matrices; the constructors were never called. Two cases now make a quaternion from each kind of matrix, translation and a turn past a half included, and hold it to the rotation the matrix was made from. getEulerAngles' case checked that the quaternion was still the identity, never the angles: it now checks the identity's angles and three round trips through setEulerAngles. A block of four checks repeated with nothing between went, a comment naming nlerp over a slerp call is corrected, and OrderToString's "not XYZ" check, made twice, is the two orders it left out. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/tests/llquaternion_test.cpp | 76 +++++++++++++++++++----- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/indra/llmath/tests/llquaternion_test.cpp b/indra/llmath/tests/llquaternion_test.cpp index 5a5bac459c..e9f5dc327c 100644 --- a/indra/llmath/tests/llquaternion_test.cpp +++ b/indra/llmath/tests/llquaternion_test.cpp @@ -28,6 +28,7 @@ #include "linden_common.h" #include +#include #include #include "../v4math.h" @@ -49,7 +50,45 @@ namespace CHECK(llquat.mQ[3] == 1.f); } + // The rotation of a matrix comes back as the quaternion it was made from, + // or its negation, which is the same rotation. + void require_same_rotation(const LLQuaternion& actual, const LLQuaternion& expected) + { + using Catch::Matchers::WithinAbs; + INFO("expected " << expected << ", got " << actual); + const F32 sign = dot(actual, expected) < 0.f ? -1.f : 1.f; + for (int i = 0; i < 4; ++i) + { + CAPTURE(i); + REQUIRE_THAT(actual.mQ[i], WithinAbs(sign * expected.mQ[i], 1e-6)); + } + } + //test case for explicit LLQuaternion(const LLMatrix4 &mat) fn. + TEST_CASE("LLQuaternion: from an LLMatrix4, the matrix's rotation, its translation ignored", "[LLQuaternion]") + { + CHECK(LLQuaternion(LLMatrix4()) == LLQuaternion::DEFAULT); + + const LLQuaternion turn(1.1f, LLVector3(2.f, -1.f, 3.f)); + require_same_rotation(LLQuaternion(LLMatrix4(turn)), turn); + require_same_rotation(LLQuaternion(LLMatrix4(turn, LLVector4(5.f, -6.f, 7.f, 1.f))), turn); + + // Past a half turn, where w is negative. + const LLQuaternion wide(4.f, LLVector3(-1.f, 0.5f, 0.25f)); + require_same_rotation(LLQuaternion(LLMatrix4(wide)), wide); + } + + //test case for explicit LLQuaternion(const LLMatrix3 &mat) fn. + TEST_CASE("LLQuaternion: from an LLMatrix3, the matrix's rotation", "[LLQuaternion]") + { + CHECK(LLQuaternion(LLMatrix3()) == LLQuaternion::DEFAULT); + + const LLQuaternion turn(1.1f, LLVector3(2.f, -1.f, 3.f)); + require_same_rotation(LLQuaternion(LLMatrix3(turn)), turn); + const LLQuaternion wide(4.f, LLVector3(-1.f, 0.5f, 0.25f)); + require_same_rotation(LLQuaternion(LLMatrix3(wide)), wide); + } + TEST_CASE("LLQuaternion: LLMatrix4 initRows fills the matrix row by row", "[LLQuaternion]") { LLMatrix4 llmat; @@ -122,12 +161,6 @@ namespace res_quat.setQuatInit(x_val, y_val, z_val, w_val); res_quat.normQuat(); - CHECK(is_approx_equal(0.0f, res_quat.mQ[0])); - CHECK(is_approx_equal(0.0f, res_quat.mQ[1])); - CHECK(is_approx_equal(0.0f, res_quat.mQ[2])); - CHECK(is_approx_equal(1.0f, res_quat.mQ[3])); - - CHECK(is_approx_equal(0.0f, res_quat.mQ[0])); CHECK(is_approx_equal(0.0f, res_quat.mQ[1])); CHECK(is_approx_equal(0.0f, res_quat.mQ[2])); @@ -229,7 +262,7 @@ namespace CHECK(is_approx_equal_fraction(0.283559f, res_nlerp.mQ[2], 16)); CHECK(is_approx_equal_fraction(0.898506f, res_nlerp.mQ[3], 16)); - //test case for nlerp(F32 t, const LLQuaternion &q) fn. + //test case for slerp(F32 u, const LLQuaternion &q) fn. res_slerp = slerp(value1, quat1); CHECK(is_approx_equal_fraction(1.0f, res_slerp.mQ[0], 16)); CHECK(is_approx_equal_fraction(2.0f, res_slerp.mQ[1], 16)); @@ -519,9 +552,10 @@ namespace result = OrderToString(LLQuaternion::ZXY); CHECK(std::string_view(result) == "ZXY"); CHECK(std::string_view(result) != "XYZ"); + CHECK(std::string_view(result) != "YZX"); + CHECK(std::string_view(result) != "XZY"); CHECK(std::string_view(result) != "YXZ"); CHECK(std::string_view(result) != "ZYX"); - CHECK(std::string_view(result) != "XYZ"); result = OrderToString(LLQuaternion::XZY); CHECK(std::string_view(result) == "XZY"); @@ -569,19 +603,33 @@ namespace CHECK(is_approx_equal_fraction(0.945733f, vect.mV[0], 16)); } - TEST_CASE("LLQuaternion: getEulerAngles leaves an identity quaternion unchanged", "[LLQuaternion]") + TEST_CASE("LLQuaternion: getEulerAngles gives back the angles setEulerAngles took", "[LLQuaternion]") { //test case for void LLQuaternion::getEulerAngles(F32 *roll, F32 *pitch, F32 *yaw) const fn + using Catch::Matchers::WithinAbs; + + // The identity has no angles, whatever the outputs held before. F32 roll = -12.0f; F32 pitch = -22.43f; F32 yaw = 11.0f; - LLQuaternion llquat; llquat.getEulerAngles(&roll, &pitch, &yaw); - CHECK(is_approx_equal(0.000f, llquat.mQ[0])); - CHECK(is_approx_equal(0.000f, llquat.mQ[1])); - CHECK(is_approx_equal(0.000f, llquat.mQ[2])); - CHECK(is_approx_equal(1.000f, llquat.mQ[3])); + CHECK_THAT(roll, WithinAbs(0.f, 1e-6)); + CHECK_THAT(pitch, WithinAbs(0.f, 1e-6)); + CHECK_THAT(yaw, WithinAbs(0.f, 1e-6)); + + // Angles within range, pitch short of a quarter turn, round trip. + const F32 angles[][3] = { { 0.3f, -0.2f, 1.1f }, { -2.5f, 1.2f, -0.4f }, { 1.f, 0.f, 0.f } }; + for (const auto& set : angles) + { + CAPTURE(set[0], set[1], set[2]); + LLQuaternion turned; + turned.setEulerAngles(set[0], set[1], set[2]); + turned.getEulerAngles(&roll, &pitch, &yaw); + CHECK_THAT(roll, WithinAbs(set[0], 1e-5)); + CHECK_THAT(pitch, WithinAbs(set[1], 1e-5)); + CHECK_THAT(yaw, WithinAbs(set[2], 1e-5)); + } } TEST_CASE("LLQuaternion: product and dot evaluate at compile time", "[LLQuaternion]") From 10e1160c2c18dc7b9565f49ceddc0c6fb187e2cb Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 23:07:59 -0400 Subject: [PATCH 4/7] mathmisc's nearestApproach test runs again, against nearest points worked in double DEV-16600 commented the test out for failing now and then. The failures were the test's: it held nearestApproach to the points it had built the two lines through, but making an LLLine in float moves it, and where the lines are near parallel that moves their nearest points by more than the tolerance. Against the nearest points worked out in double from the lines as made, nearestApproach held for 500,000 random pairs over 50 seeds, where against the built points 5 seeds in 20 failed. The case runs again, with two lines whose nearest points are known exactly, and the random pairs at a fixed seed held to the reference. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/tests/mathmisc_test.cpp | 74 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/indra/llmath/tests/mathmisc_test.cpp b/indra/llmath/tests/mathmisc_test.cpp index 70dd912dee..407f77fe31 100644 --- a/indra/llmath/tests/mathmisc_test.cpp +++ b/indra/llmath/tests/mathmisc_test.cpp @@ -37,6 +37,7 @@ #include "../llline.h" #include "../llmath.h" #include "../v3math.h" +#include "../v3dmath.h" #include @@ -235,7 +236,8 @@ namespace struct line_data { - // The intersects and two-planes cases seed the main thread's generator. + // The intersects, nearest-approach and two-planes cases seed the main + // thread's generator. ~line_data() { ll_test::reseedRandomFromEntropy(); } }; @@ -303,23 +305,38 @@ namespace } } - TEST_CASE_METHOD(line_data, "LLLine: nearestApproach is disabled for DEV-16600", "[LLLine]") + TEST_CASE_METHOD(line_data, "LLLine: nearestApproach finds each line's point nearest the other", "[LLLine]") { - /* - These tests fail intermittently on all platforms - see DEV-16600 - Commenting this out until dev has time to investigate. - - // this is a test for LLLine::nearestApproach(LLLIne) method + // this is a test for LLLine::nearestApproach(LLLine) method // which computes the point on a line nearest another line - // these tests will have some floating point error, - // so we need to specify how much error is ok - // TODO -- make nearestApproach() algorithm more accurate so - // we can tighten the allowable_error. Most tests are tighter - // than one milimeter, however when doing randomized testing - // you can walk into inaccurate cases. + // Two lines whose nearest points are known exactly: the x axis, and + // a line along y through (3, 0, 5). + LLLine x_axis(LLVector3(0.f, 0.f, 0.f), LLVector3(1.f, 0.f, 0.f)); + LLLine above(LLVector3(3.f, -1.f, 5.f), LLVector3(3.f, 1.f, 5.f)); + CHECK(dist_vec(x_axis.nearestApproach(above), LLVector3(3.f, 0.f, 0.f)) < 1e-5f); + CHECK(dist_vec(above.nearestApproach(x_axis), LLVector3(3.f, 0.f, 5.f)) < 1e-5f); + + // Random lines, held to the nearest points worked out in double from + // the lines as they were made. Not to the points they were built + // through: making a line in float moves it, and where the two lines + // are near parallel that moves their nearest points by far more than + // nearestApproach's own error. That, not nearestApproach, was the + // intermittent failure DEV-16600 commented this test out for. + auto nearest_in_double = [](const LLLine& line, const LLLine& other) + { + const LLVector3d p1(line.getPoint()), d1(line.getDirection()); + const LLVector3d p2(other.getPoint()), d2(other.getDirection()); + const LLVector3d between = p1 - p2; + const F64 aa = d1 * d1, ab = d1 * d2, bb = d2 * d2, a_between = d1 * between, b_between = d2 * between; + const F64 along = (ab * b_between - bb * a_between) / (aa * bb - ab * ab); + return LLVector3(p1 + d1 * along); + }; + F32 allowable_relative_error = 0.001f; S32 number_of_tests = 100; + // A fixed seed, so a failure draws the same lines when run again. + ll_rand_seed(16600); for (S32 test = 0; test < number_of_tests; ++test) { // generate two points to be our known nearest approaches @@ -375,6 +392,10 @@ namespace LLVector3 some_computed_point = first_line.nearestApproach(second_line); LLVector3 another_computed_point = second_line.nearestApproach(first_line); + // what they should be, for the lines as made + some_point = nearest_in_double(first_line, second_line); + another_point = nearest_in_double(second_line, first_line); + // compute the error F32 first_error = (some_point - some_computed_point).length(); F32 scale = llmax((some_point - another_point).length(), some_point.length()); @@ -385,32 +406,11 @@ namespace F32 second_error = (another_point - another_computed_point).length(); F32 second_relative_error = second_error / scale; - //if (first_relative_error > allowable_relative_error) - //{ - // std::cout << "first_error = " << first_error - // << " first_relative_error = " << first_relative_error - // << " scale = " << scale - // << " dir_dot = " << (first_dir * second_dir) - // << std::endl; - //} - //if (second_relative_error > allowable_relative_error) - //{ - // std::cout << "second_error = " << second_error - // << " second_relative_error = " << second_relative_error - // << " scale = " << scale - // << " dist = " << (some_point - another_point).length() - // << " dir_dot = " << (first_dir * second_dir) - // << std::endl; - //} - // test that the errors are small - - ensure("first line should accurately compute its closest approach", - first_relative_error <= allowable_relative_error); - ensure("second line should accurately compute its closest approach", - second_relative_error <= allowable_relative_error); + CAPTURE(test, scale, first_dir * second_dir); + REQUIRE(first_relative_error <= allowable_relative_error); + REQUIRE(second_relative_error <= allowable_relative_error); } - */ } F32 ALMOST_PARALLEL = 0.99f; From 938ef0f450d5b074342b9a975d9e5f9f2f246ab8 Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 11 Oct 2026 23:08:00 -0400 Subject: [PATCH 5/7] llsdutil_math loses a helper nothing called, and m3math a repeated pragma llsdutil_math's fixture held test_matches, a copy of llcommon's llsdutil_test helper for llsd_matches, which llcommon tests and no case here called; the fixture held nothing else, so the cases are plain TEST_CASEs and the includes only it used go. m3math disabled C4702 in two identical blocks. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/tests/llsdutil_math_test.cpp | 47 +++-------------------- indra/llmath/tests/m3math_test.cpp | 5 --- 2 files changed, 5 insertions(+), 47 deletions(-) diff --git a/indra/llmath/tests/llsdutil_math_test.cpp b/indra/llmath/tests/llsdutil_math_test.cpp index 0314c32592..afe8efb194 100644 --- a/indra/llmath/tests/llsdutil_math_test.cpp +++ b/indra/llmath/tests/llsdutil_math_test.cpp @@ -31,54 +31,17 @@ #include "altestmatchers.h" #include "m4math.h" #include "v2math.h" -#include "v2math.h" #include "v3color.h" #include "v3math.h" #include "v3dmath.h" #include "v4coloru.h" #include "v4math.h" #include "llquaternion.h" -#include "llsdutil.h" #include "llsdutil_math.h" -#include "stringize.h" -#include -#include namespace { - struct llsdutil_math_data - { - void test_matches(const std::string& proto_key, const LLSD& possibles, - const char** begin, const char** end) - { - std::set succeed(begin, end); - LLSD prototype(possibles[proto_key]); - for (LLSD::map_const_iterator pi(possibles.beginMap()), pend(possibles.endMap()); - pi != pend; ++pi) - { - std::string match(llsd_matches(prototype, pi->second)); - std::set::const_iterator found = succeed.find(pi->first); - if (found != succeed.end()) - { - // This test is supposed to succeed. Comparing to the - // empty string ensures that if the test fails, it will - // display the string received so we can tell what failed. - REQUIRE(match == ""); - } - else - { - // This test is supposed to fail. If we get a false match, - // the string 'match' will be empty, which doesn't tell us - // much about which case went awry. So construct a more - // detailed description string. - INFO(proto_key << " shouldn't match " << pi->first); - REQUIRE_FALSE(match.empty()); - } - } - } - }; - - TEST_CASE_METHOD(llsdutil_math_data, "llsdutil_math: LLVector3 round-trips through LLSD", "[llsdutil_math]") + TEST_CASE("llsdutil_math: LLVector3 round-trips through LLSD", "[llsdutil_math]") { LLSD sd; LLVector3 vec1(-1.0, 2.0, -3.0); @@ -97,7 +60,7 @@ namespace sd.clear(); } - TEST_CASE_METHOD(llsdutil_math_data, "llsdutil_math: LLVector3d round-trips through LLSD", "[llsdutil_math]") + TEST_CASE("llsdutil_math: LLVector3d round-trips through LLSD", "[llsdutil_math]") { LLSD sd; LLVector3d vec1((F64)(U64L(0xFEDCBA9876543210) << 2), -1., 0); @@ -109,7 +72,7 @@ namespace CHECK(vec1 == vec3); } - TEST_CASE_METHOD(llsdutil_math_data, "llsdutil_math: LLVector2 and its LLSD round-trip both ways", "[llsdutil_math]") + TEST_CASE("llsdutil_math: LLVector2 and its LLSD round-trip both ways", "[llsdutil_math]") { LLSD sd; LLVector2 vec((F32) -3., (F32) 4.2); @@ -121,7 +84,7 @@ namespace CHECK_THAT(sd, ll_test::LLSDEquals(sd2)); } - TEST_CASE_METHOD(llsdutil_math_data, "llsdutil_math: LLQuaternion and its LLSD round-trip both ways", "[llsdutil_math]") + TEST_CASE("llsdutil_math: LLQuaternion and its LLSD round-trip both ways", "[llsdutil_math]") { LLSD sd; LLQuaternion quat((F32) 1., (F32) -0.98, (F32) 2.3, (F32) 0xffff); @@ -133,7 +96,7 @@ namespace CHECK_THAT(sd, ll_test::LLSDEquals(sd2)); } - TEST_CASE_METHOD(llsdutil_math_data, "llsdutil_math: LLColor4 and its LLSD round-trip both ways", "[llsdutil_math]") + TEST_CASE("llsdutil_math: LLColor4 and its LLSD round-trip both ways", "[llsdutil_math]") { LLSD sd; LLColor4 c(1.0f, 2.2f, 4.0f, 7.f); diff --git a/indra/llmath/tests/m3math_test.cpp b/indra/llmath/tests/m3math_test.cpp index 35b6ba611c..fe22e51043 100644 --- a/indra/llmath/tests/m3math_test.cpp +++ b/indra/llmath/tests/m3math_test.cpp @@ -42,11 +42,6 @@ #pragma warning(disable: 4702) #endif -#if LL_WINDOWS -// disable unreachable code warnings caused by usage of skip. -#pragma warning(disable: 4702) -#endif - namespace { //test case for setIdentity() fn. From c3f1e43e9dc8558d5db9afe8c66dba61e3499966 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 12 Oct 2026 03:08:53 -0400 Subject: [PATCH 6/7] LLSDEquals holds a NaN equal to nothing under MSVC's /fp:fast too The optimised Windows builds compile with /fp:fast, under which MSVC may compile == without its test for an unordered result. A NaN then compares equal to any number. LLSDEquals compared reals and dates' seconds with ==, so on Windows x64 a NaN date matched LLDate(0.0), and test_support_tests failed there, as it has on develop since #446. clang and GCC keep IEEE comparisons under the flags the tree gives them, so no other row saw it. Reals and dates now go through a compare that asks about NaN apart, with std::isnan, which classifies the bits and so holds under /fp:fast. The harness's tests check a NaN real as well as a NaN date. Co-Authored-By: Claude Opus 5.5 --- indra/test/altestmatchers.cpp | 14 ++++++++++++-- indra/test/tests/altestsupport_test.cpp | 3 +++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/indra/test/altestmatchers.cpp b/indra/test/altestmatchers.cpp index 633a7ce47a..b790f5754f 100644 --- a/indra/test/altestmatchers.cpp +++ b/indra/test/altestmatchers.cpp @@ -30,12 +30,22 @@ #include "llsdserialize.h" #include +#include #include namespace ll_test { namespace { + // A NaN equals nothing, itself included. Under /fp:fast, which the + // optimised Windows builds use, MSVC compiles == without its test for + // an unordered result, so a NaN would equal anything: it is asked + // about apart. + bool sameNumber(F64 actual, F64 expected) + { + return !std::isnan(actual) && !std::isnan(expected) && actual == expected; + } + // Where actual and expected first differ: a path from the top, as in // ["key"][3], and what differs there. Empty "what" means equal. struct Difference @@ -62,7 +72,7 @@ namespace ll_test same = actual.asInteger() == expected.asInteger(); break; case LLSD::TypeReal: - same = actual.asReal() == expected.asReal(); + same = sameNumber(actual.asReal(), expected.asReal()); break; case LLSD::TypeString: same = actual.asString() == expected.asString(); @@ -73,7 +83,7 @@ namespace ll_test case LLSD::TypeDate: // Not LLDate's ==, which is made of < and so holds a NaN // date equal to every date; TUT compared the seconds. - same = actual.asDate().secondsSinceEpoch() == expected.asDate().secondsSinceEpoch(); + same = sameNumber(actual.asDate().secondsSinceEpoch(), expected.asDate().secondsSinceEpoch()); break; case LLSD::TypeURI: same = actual.asURI().asString() == expected.asURI().asString(); diff --git a/indra/test/tests/altestsupport_test.cpp b/indra/test/tests/altestsupport_test.cpp index d8c1283ad7..d0b28927ac 100644 --- a/indra/test/tests/altestsupport_test.cpp +++ b/indra/test/tests/altestsupport_test.cpp @@ -101,6 +101,9 @@ TEST_CASE("LLSDEquals compares all the way down and says where two differ", "[al // LLDate's own == holds a NaN date equal to any. const LLSD nan_date = LLDate(std::numeric_limits::quiet_NaN()); CHECK_FALSE(ll_test::LLSDEquals(LLDate(0.0)).match(nan_date)); + // Nor a NaN number, which MSVC's /fp:fast compare would hold equal. + const LLSD nan_real = std::numeric_limits::quiet_NaN(); + CHECK_FALSE(ll_test::LLSDEquals(0.0).match(nan_real)); CHECK_THAT(LLSD(LLDate(1.0)), ll_test::LLSDEquals(LLDate(1.0))); } From a5472d08916afd87fce812ccaa793e0a929f7072 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 12 Oct 2026 03:08:53 -0400 Subject: [PATCH 7/7] v4math's quaternion rotVec check turns a vector off the turn's axis The vector it turned was the quaternion's own axis, which a turn leaves where it is, so a rotVec that did nothing would have passed. It turns (1, 2, 3) now, which the turn moves: with the rotVec call taken out, the check fails. Pointed out in review. Co-Authored-By: Claude Opus 5.5 --- indra/llmath/tests/v4math_test.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/indra/llmath/tests/v4math_test.cpp b/indra/llmath/tests/v4math_test.cpp index 80e93d4d4a..f3d1e10526 100644 --- a/indra/llmath/tests/v4math_test.cpp +++ b/indra/llmath/tests/v4math_test.cpp @@ -231,10 +231,11 @@ namespace CHECK_THAT(quarter.mV[VZ], WithinAbs(0.f, 1e-6)); CHECK(quarter.mV[VW] == 3.f); + // A vector off the turn's axis, which the turn moves. const LLQuaternion q(1.3f, LLVector3(2.32f, -23.2f, -34.1112f)); - LLVector4 vec4b(2.32f, -23.2f, -34.1112f, 1.010112f); + LLVector4 vec4b(1.f, 2.f, 3.f, 1.010112f); vec4b.rotVec(q); - const LLVector3 turned = LLVector3(2.32f, -23.2f, -34.1112f) * q; + const LLVector3 turned = LLVector3(1.f, 2.f, 3.f) * q; CHECK(vec4b.mV[VX] == turned.mV[VX]); CHECK(vec4b.mV[VY] == turned.mV[VY]); CHECK(vec4b.mV[VZ] == turned.mV[VZ]);