fix(coverage+validation): C2/C3 script gate, V1/V2/V4 JSON/XML errors, I2/I3/I4 tests
C2: Fix coverage.sh — remove || true from lcov capture/extract steps so real
lcov failures are visible; empty coverage.info now exits with code 3.
C3: Add coverage gate to quality-gates CI job (SKIP_COVERAGE_GATE=1 ramp-up
mode until I5 is resolved — fast suite covers ~9.6% not 80%). Thresholds:
80% line / 70% branch / 90% function (agreed 2026-05-31).
V1: Wrap JSON parse + field extraction in try/catch — nlohmann parse_error and
type_error now surface as std::runtime_error with the file path.
V2: Wrap stoi/stod in XML Solver parser — missing/non-numeric attributes throw
std::runtime_error instead of leaking std::invalid_argument.
V4: Validate required JSON keys (dof_vector, solver, solver.*) before access —
missing field produces a clear named-field error message.
I2: 6 serialization negative tests (missing file, malformed JSON, missing
dof_vector, missing solver block, missing XML file, non-numeric XML attr).
I3: load_mesh throws on non-triangulated (quad) mesh — covers the
is_triangle_mesh guard that was previously untested.
I4: spherical_hessian throws on edge DOFs — covers the logic_error guard.
290/290 tests pass (+8 new).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
#include "serialization.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
|
||||
@@ -370,3 +371,67 @@ TEST(Serialization, XML_RoundTrip)
|
||||
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// I2: serialization throws on malformed / schema-violating input
|
||||
//
|
||||
// These tests cover the throw paths in load_result_json / load_result_xml
|
||||
// that were previously untested (I2 in the test-coverage audit).
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(Serialization, LoadResultJson_ThrowsOnMissingFile)
|
||||
{
|
||||
EXPECT_THROW(load_result_json("/tmp/does_not_exist_conflab.json"),
|
||||
std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(Serialization, LoadResultJson_ThrowsOnMalformedJson)
|
||||
{
|
||||
const std::string path = "/tmp/conflab_bad.json";
|
||||
{ std::ofstream ofs(path); ofs << "{not valid json!!!"; }
|
||||
EXPECT_THROW(load_result_json(path), std::runtime_error);
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
TEST(Serialization, LoadResultJson_ThrowsOnMissingDofVector)
|
||||
{
|
||||
// V4: a valid JSON object but without the required "dof_vector" key.
|
||||
const std::string path = "/tmp/conflab_nodof.json";
|
||||
{ std::ofstream ofs(path); ofs << R"({"geometry":"euclidean"})"; }
|
||||
EXPECT_THROW(load_result_json(path), std::runtime_error);
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
TEST(Serialization, LoadResultJson_ThrowsOnMissingSolverField)
|
||||
{
|
||||
// V4: has dof_vector but solver block is absent when res != nullptr.
|
||||
const std::string path = "/tmp/conflab_nosolver.json";
|
||||
{ std::ofstream ofs(path); ofs << R"({"dof_vector":[0.1,0.2]})"; }
|
||||
NewtonResult res;
|
||||
EXPECT_THROW(load_result_json(path, &res), std::runtime_error);
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
TEST(Serialization, LoadResultXml_ThrowsOnMissingFile)
|
||||
{
|
||||
EXPECT_THROW(load_result_xml("/tmp/does_not_exist_conflab.xml"),
|
||||
std::runtime_error);
|
||||
}
|
||||
|
||||
TEST(Serialization, LoadResultXml_ThrowsOnMalformedSolverAttribute)
|
||||
{
|
||||
// V2: non-numeric attribute causes stoi/stod error that must surface
|
||||
// as runtime_error, not std::invalid_argument.
|
||||
const std::string path = "/tmp/conflab_badxml.xml";
|
||||
{
|
||||
std::ofstream ofs(path);
|
||||
ofs << R"(<?xml version="1.0"?>
|
||||
<ConformalResult geometry="euclidean" vertices="4" faces="4">
|
||||
<Solver converged="true" iterations="not_a_number" grad_inf_norm="1e-9"/>
|
||||
<DOFVector n="1">0.0</DOFVector>
|
||||
</ConformalResult>)";
|
||||
}
|
||||
NewtonResult res;
|
||||
EXPECT_THROW(load_result_xml(path, &res), std::runtime_error);
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
@@ -112,6 +112,30 @@ TEST(MeshIO, LoadMeshThrowsOnMissingFile)
|
||||
EXPECT_THROW(load_mesh("/tmp/does_not_exist_conflab.off"), std::runtime_error);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// I3: load_mesh throws on non-triangulated mesh
|
||||
//
|
||||
// The quad-strip mesh has 4-vertex faces; load_mesh must reject it at the
|
||||
// I/O boundary rather than letting the quad faces flow silently into the
|
||||
// triangle-only functionals.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(MeshIO, LoadMeshThrowsOnNonTriangulatedMesh)
|
||||
{
|
||||
// Write a minimal OFF quad-face mesh (2 quads, not triangles).
|
||||
auto path = tmp_path("quad.off");
|
||||
{
|
||||
std::ofstream ofs(path);
|
||||
ofs << "OFF\n6 2 0\n"
|
||||
<< "0 0 0\n1 0 0\n1 1 0\n0 1 0\n"
|
||||
<< "2 0 0\n2 1 0\n"
|
||||
<< "4 0 1 2 3\n"
|
||||
<< "4 1 4 5 2\n";
|
||||
}
|
||||
EXPECT_THROW(load_mesh(path), std::runtime_error);
|
||||
rm(path);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Vertex positions survive a round-trip (OFF)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -206,3 +206,31 @@ TEST(SphericalHessian, FDCheck_MixedPinnedVertices)
|
||||
EXPECT_TRUE(hessian_check_spherical(mesh, x, maps))
|
||||
<< "FD Hessian check failed for mixed pinned/variable vertices";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// I4: spherical_hessian throws on edge DOFs
|
||||
//
|
||||
// The spherical Hessian only implements the vertex-block cotangent Laplacian;
|
||||
// if any edge has a free DOF index (e_idx >= 0) it must fail loudly rather
|
||||
// than returning a silently rank-deficient matrix.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SphericalHessian, ThrowsOnEdgeDOF)
|
||||
{
|
||||
// Build a tetrahedron and assign one edge as a free DOF.
|
||||
auto mesh = make_tetrahedron();
|
||||
auto maps = setup_spherical_maps(mesh);
|
||||
compute_lambda0_from_mesh(mesh, maps); // spherical variant (A2 rename pending)
|
||||
|
||||
int idx = 0;
|
||||
for (auto v : mesh.vertices())
|
||||
maps.v_idx[v] = idx++;
|
||||
const int n_v = idx;
|
||||
|
||||
// Give one edge a free DOF index to trigger the guard.
|
||||
auto e0 = *mesh.edges().begin();
|
||||
maps.e_idx[e0] = n_v; // first free edge DOF
|
||||
|
||||
std::vector<double> x(static_cast<std::size_t>(n_v + 1), 0.0);
|
||||
EXPECT_THROW(spherical_hessian(mesh, x, maps), std::logic_error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user