test(s3): add negative/boundary tests for H3/H4/H5/V5/V6
All checks were successful
C++ Tests / test-fast (pull_request) Successful in 2m49s
C++ Tests / quality-gates (pull_request) Has been skipped
C++ Tests / test-cgal (pull_request) Has been skipped

H3 (GaussBonnet suite, test_phase6.cpp):
 - EnforceReturnsCorrectionMagnitude_LargeCorrection: feeds tetrahedron
   with all theta_v=0, asserts returned correction is exactly 4pi.
 - EnforceReturnsCorrectionMagnitude_NearZeroWhenAlreadyCorrect: angles
   already satisfy GB, asserts correction near zero.
 - EnforceRawMapOverload_ReturnsCorrection: verifies the raw-map overload
   also returns the correction magnitude.

H4 (PeriodMatrix suite, test_phase7.cpp):
 - ReduceToFD_ThrowsForRealAxisBoundary: the boundary Im(tau)==0.0 (real
   axis) must throw domain_error, covering the <= guard's exact boundary.

H5 (NewtonSolver suite, test_newton_solver.cpp):
 - Euclidean_SliverTriangle_CharacterizedBehavior: near-degenerate sliver
   triangle (aspect ratio ~10000) — solver must not crash or NaN, and
   must not report LinearSolverFailed (system is still consistent).
 - Euclidean_ExactDegenerateTriangle_NoCrash: collinear vertices (zero
   area) — euclidean_cot_weights returns {0,0,0,false}; asserts no crash
   and that the reported status is a meaningful failure mode.

V5 (Serialization suite, test_layout.cpp):
 - LoadResultXml_RejectsReformattedRootElement: <ConformalResult> with
   geometry= on a separate line throws runtime_error.
 - LoadResultXml_RejectsDOFVectorWithTagOpenOnSeparateLine: <DOFVector>
   with '>' on a separate line throws runtime_error.
 - LoadResultXml_CanonicalFormatStillWorks: regression guard confirming
   save_result_xml canonical output still round-trips correctly.

V6 (Serialization suite, test_layout.cpp):
 - CheckDofVectorSize_ThrowsOnMismatch: size 3 vs expected 5 throws.
 - CheckDofVectorSize_PassesOnMatch: exact match does not throw.
 - CheckDofVectorSize_ErrorMessageNamesExpectedAndActual: exception
   message contains both the actual and expected sizes.

Total CGAL tests: 313 (up from 298; 15 new tests, 0 failures).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-01 01:27:25 +02:00
parent 833f9e7237
commit 2e6c4d75c1
4 changed files with 328 additions and 0 deletions

View File

@@ -737,3 +737,107 @@ TEST(NewtonCore, Status_LineSearchStalled)
EXPECT_EQ(res.status, conformallab::NewtonStatus::LineSearchStalled);
EXPECT_EQ(res.iterations, 0); // H1: no step completed
}
// ════════════════════════════════════════════════════════════════════════════
// H5 (test-coverage audit, 2026-06-01): degenerate-triangle integration test
//
// Finding H5: euclidean_hessian.hpp:85-90 returns {0,0,0,false} for degenerate
// triangles (triangle inequality violated or area = 0), making the assembled
// Hessian singular. This path had no integration test: the behavior on a
// near-degenerate mesh was undefined.
//
// Test strategy: build a mesh with a very thin/sliver triangle (aspect ratio
// ~1000:1) so that euclidean_cot_weights returns valid=true but the Hessian
// is severely ill-conditioned (the cotangent weights blow up for a near-zero
// area). Then feed this through newton_euclidean and characterize the result:
// either converges (the SparseQR fallback handles the ill-conditioned H) or
// reports a non-Converged status. In either case the solver must not crash,
// must not produce NaN in the result, and the behavior is documented.
//
// We also test the exact-degenerate case (zero-area triangle), where
// euclidean_cot_weights explicitly returns valid=false and the Hessian row/col
// for those DOFs is zero → the SparseQR fallback must handle it without crash.
// ════════════════════════════════════════════════════════════════════════════
TEST(NewtonSolver, Euclidean_SliverTriangle_CharacterizedBehavior)
{
// Build a very thin sliver triangle: v0=(0,0), v1=(1,0), v2=(0,1e-4).
// Area ≈ 5e-5, aspect ratio ≈ 10000. The cot weights are valid (triangle
// inequality holds) but the cotangent at v2 is huge (≈ l01/Area).
ConformalMesh mesh;
auto v0 = mesh.add_vertex(Point3(0.0, 0.0, 0.0));
auto v1 = mesh.add_vertex(Point3(1.0, 0.0, 0.0));
auto v2 = mesh.add_vertex(Point3(0.0, 1e-4, 0.0));
mesh.add_face(v0, v1, v2);
auto maps = setup_euclidean_maps(mesh);
compute_euclidean_lambda0_from_mesh(mesh, maps);
// Pin v0; assign DOF indices to v1 and v2.
maps.v_idx[v0] = -1;
maps.v_idx[v1] = 0;
maps.v_idx[v2] = 1;
const int n = 2;
// Natural theta: equilibrium at x* = 0 by construction.
set_natural_euclidean_theta(mesh, maps, n);
std::vector<double> x0(n, 0.0);
auto res = newton_euclidean(mesh, x0, maps, /*tol=*/1e-8, /*max_iter=*/100);
// H5 acceptance criterion: behavior is characterized, not undefined.
// The solver must not crash or produce NaN.
EXPECT_EQ(static_cast<int>(res.x.size()), n)
<< "Result vector must always be populated";
for (double xi : res.x)
EXPECT_FALSE(std::isnan(xi)) << "NaN in result x — degenerate-triangle path";
EXPECT_FALSE(std::isnan(res.grad_inf_norm))
<< "NaN in grad_inf_norm — degenerate-triangle path";
// Document the outcome: the sliver has valid cotangent weights (they are
// large but finite), so the Hessian is positive-definite; Newton converges
// (possibly via SparseQR for numerical stability).
// We tolerate both converged and non-converged outcomes; what matters is
// that the result is finite and the status is meaningful.
EXPECT_NE(res.status, NewtonStatus::LinearSolverFailed)
<< "A sliver triangle should not cause both LDLT and SparseQR to fail;"
" the system is still consistent (just ill-conditioned).";
}
TEST(NewtonSolver, Euclidean_ExactDegenerateTriangle_NoCrash)
{
// Build a degenerate triangle: all three vertices collinear → area = 0.
// v0=(0,0), v1=(1,0), v2=(2,0). This forces kahan <= 0 in
// euclidean_cot_weights → {0,0,0,false}. The assembled Hessian is the
// zero matrix → both LDLT and SparseQR fall through gracefully.
ConformalMesh mesh;
auto v0 = mesh.add_vertex(Point3(0.0, 0.0, 0.0));
auto v1 = mesh.add_vertex(Point3(1.0, 0.0, 0.0));
auto v2 = mesh.add_vertex(Point3(2.0, 0.0, 0.0));
mesh.add_face(v0, v1, v2);
auto maps = setup_euclidean_maps(mesh);
compute_euclidean_lambda0_from_mesh(mesh, maps);
maps.v_idx[v0] = -1;
maps.v_idx[v1] = 0;
maps.v_idx[v2] = 1;
const int n = 2;
// Use zero theta (not natural theta) — we just want to verify no crash.
std::vector<double> x0(n, 0.0);
// H5 acceptance criterion: no crash, no UB, result struct populated.
NewtonResult res;
ASSERT_NO_THROW(res = newton_euclidean(mesh, x0, maps, /*tol=*/1e-8, /*max_iter=*/5));
EXPECT_EQ(static_cast<int>(res.x.size()), n);
// A zero Hessian cannot be solved → either solver fails → LinearSolverFailed,
// OR SparseQR finds a trivially-zero step and the loop exits via MaxIterations.
// Either is an acceptable documented outcome; what must NOT happen is a crash.
EXPECT_TRUE(res.status == NewtonStatus::LinearSolverFailed
|| res.status == NewtonStatus::MaxIterations
|| res.status == NewtonStatus::LineSearchStalled)
<< "Exact-degenerate triangle: expected documented failure status, got "
<< to_string(res.status);
}