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:
Tarik Moussa
2026-05-31 14:43:00 +02:00
parent 51d9844f7a
commit a5718c0326
6 changed files with 274 additions and 45 deletions

View File

@@ -93,31 +93,60 @@ inline std::vector<double> load_result_json(
{
using json = nlohmann::json;
std::ifstream ifs(path);
if (!ifs) throw std::runtime_error("Cannot open: " + path);
json j; ifs >> j;
if (!ifs) throw std::runtime_error("conformallab: cannot open: " + path);
if (geom && j.contains("geometry"))
*geom = j["geometry"].get<std::string>();
std::vector<double> x = j.at("dof_vector").get<std::vector<double>>();
if (res) {
res->x = x;
res->converged = j["solver"]["converged"].get<bool>();
res->iterations = j["solver"]["iterations"].get<int>();
res->grad_inf_norm = j["solver"]["grad_inf_norm"].get<double>();
// V1: wrap parse + field extraction so nlohmann exceptions (parse_error,
// type_error, out_of_range) surface as std::runtime_error with the path.
json j;
try {
ifs >> j;
} catch (const json::exception& e) {
throw std::runtime_error(
"conformallab: malformed JSON in " + path + ": " + e.what());
}
if (layout2d && j.contains("layout") && j["layout"]["dim"] == 2) {
auto uv = j["layout"]["uv"];
layout2d->uv.resize(uv.size());
for (std::size_t i = 0; i < uv.size(); ++i)
layout2d->uv[i] = { uv[i][0].get<double>(), uv[i][1].get<double>() };
layout2d->has_seam = j["layout"].value("has_seam", false);
layout2d->success = true;
}
try {
if (geom && j.contains("geometry"))
*geom = j["geometry"].get<std::string>();
return x;
// V4: validate required top-level key before accessing it.
if (!j.contains("dof_vector"))
throw std::runtime_error(
"conformallab: result JSON missing field 'dof_vector' in " + path);
std::vector<double> x = j.at("dof_vector").get<std::vector<double>>();
if (res) {
// V4: validate nested solver keys before accessing.
if (!j.contains("solver"))
throw std::runtime_error(
"conformallab: result JSON missing field 'solver' in " + path);
const auto& s = j.at("solver");
for (const char* key : {"converged", "iterations", "grad_inf_norm"}) {
if (!s.contains(key))
throw std::runtime_error(
std::string("conformallab: result JSON missing field 'solver.")
+ key + "' in " + path);
}
res->x = x;
res->converged = s.at("converged").get<bool>();
res->iterations = s.at("iterations").get<int>();
res->grad_inf_norm = s.at("grad_inf_norm").get<double>();
}
if (layout2d && j.contains("layout") && j["layout"]["dim"] == 2) {
auto uv = j["layout"]["uv"];
layout2d->uv.resize(uv.size());
for (std::size_t i = 0; i < uv.size(); ++i)
layout2d->uv[i] = { uv[i][0].get<double>(), uv[i][1].get<double>() };
layout2d->has_seam = j["layout"].value("has_seam", false);
layout2d->success = true;
}
return x;
} catch (const json::exception& e) {
throw std::runtime_error(
"conformallab: malformed JSON in " + path + ": " + e.what());
}
}
// ════════════════════════════════════════════════════════════════════════════
@@ -255,9 +284,23 @@ inline std::vector<double> load_result_xml(
// Solver metadata
else if (line.find("<Solver") != std::string::npos) {
if (res) {
res->converged = (detail_xml::xml_get_attr(line, "converged") == "true");
res->iterations = std::stoi(detail_xml::xml_get_attr(line, "iterations"));
res->grad_inf_norm = std::stod(detail_xml::xml_get_attr(line, "grad_inf_norm"));
res->converged = (detail_xml::xml_get_attr(line, "converged") == "true");
// V2: stoi/stod throw std::invalid_argument on empty or non-numeric
// attribute values; wrap and rethrow as runtime_error with context.
try {
auto iter_str = detail_xml::xml_get_attr(line, "iterations");
auto grad_str = detail_xml::xml_get_attr(line, "grad_inf_norm");
if (iter_str.empty())
throw std::runtime_error("missing attribute 'iterations'");
if (grad_str.empty())
throw std::runtime_error("missing attribute 'grad_inf_norm'");
res->iterations = std::stoi(iter_str);
res->grad_inf_norm = std::stod(grad_str);
} catch (const std::exception& e) {
throw std::runtime_error(
"conformallab: malformed XML Solver element in "
+ path + ": " + e.what());
}
}
}
// DOF vector