// Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // test_mesh_io.cpp // // Phase 4b — CGAL::IO mesh round-trip tests. // // Tests: // 1. OFF write + read round-trip: vertex count, face count, edge count preserved. // 2. OBJ write + read round-trip: same topology checks. // 3. load_mesh() throws on non-existent file. // 4. Round-trip preserves vertex positions (within floating-point precision). #include "conformal_mesh.hpp" #include "mesh_builder.hpp" #include "mesh_io.hpp" #include #include #include #include #include using namespace conformallab; // Helper: temp file path with given extension static std::string tmp_path(const char* ext) { return std::string("/tmp/conformallab_test.") + ext; } // Helper: delete file if it exists static void rm(const std::string& path) { std::filesystem::remove(path); } // ════════════════════════════════════════════════════════════════════════════ // OFF round-trip: tetrahedron // ════════════════════════════════════════════════════════════════════════════ TEST(MeshIO, OFF_RoundTrip_Tetrahedron) { auto path = tmp_path("off"); rm(path); auto mesh_out = make_tetrahedron(); ASSERT_TRUE(write_mesh(path, mesh_out)) << "write_mesh should succeed for tetrahedron"; ConformalMesh mesh_in; ASSERT_TRUE(read_mesh(path, mesh_in)) << "read_mesh should succeed for the written OFF file"; EXPECT_EQ(mesh_in.number_of_vertices(), mesh_out.number_of_vertices()); EXPECT_EQ(mesh_in.number_of_faces(), mesh_out.number_of_faces()); EXPECT_EQ(mesh_in.number_of_edges(), mesh_out.number_of_edges()); rm(path); } // ════════════════════════════════════════════════════════════════════════════ // OFF round-trip: quad strip // ════════════════════════════════════════════════════════════════════════════ TEST(MeshIO, OFF_RoundTrip_QuadStrip) { auto path = tmp_path("off"); rm(path); auto mesh_out = make_quad_strip(); ASSERT_TRUE(write_mesh(path, mesh_out)); ConformalMesh mesh_in; ASSERT_TRUE(read_mesh(path, mesh_in)); EXPECT_EQ(mesh_in.number_of_vertices(), mesh_out.number_of_vertices()); EXPECT_EQ(mesh_in.number_of_faces(), mesh_out.number_of_faces()); rm(path); } // ════════════════════════════════════════════════════════════════════════════ // OBJ round-trip: tetrahedron // ════════════════════════════════════════════════════════════════════════════ TEST(MeshIO, OBJ_RoundTrip_Tetrahedron) { auto path = tmp_path("obj"); rm(path); auto mesh_out = make_tetrahedron(); ASSERT_TRUE(write_mesh(path, mesh_out)) << "write_mesh (OBJ) should succeed"; ConformalMesh mesh_in; ASSERT_TRUE(read_mesh(path, mesh_in)) << "read_mesh (OBJ) should succeed"; EXPECT_EQ(mesh_in.number_of_vertices(), mesh_out.number_of_vertices()); EXPECT_EQ(mesh_in.number_of_faces(), mesh_out.number_of_faces()); rm(path); } // ════════════════════════════════════════════════════════════════════════════ // load_mesh throws on missing file // ════════════════════════════════════════════════════════════════════════════ TEST(MeshIO, LoadMeshThrowsOnMissingFile) { EXPECT_THROW(load_mesh("/tmp/does_not_exist_conflab.off"), std::runtime_error); } // ════════════════════════════════════════════════════════════════════════════ // Vertex positions survive a round-trip (OFF) // ════════════════════════════════════════════════════════════════════════════ TEST(MeshIO, OFF_VertexPositionsPreserved) { auto path = tmp_path("off"); rm(path); auto mesh_out = make_tetrahedron(); ASSERT_TRUE(write_mesh(path, mesh_out)); ConformalMesh mesh_in; ASSERT_TRUE(read_mesh(path, mesh_in)); // Collect and sort vertex positions from both meshes to compare auto collect_sorted = [](const ConformalMesh& m) { std::vector> pts; for (auto v : m.vertices()) { auto p = m.point(v); pts.push_back({CGAL::to_double(p.x()), CGAL::to_double(p.y()), CGAL::to_double(p.z())}); } std::sort(pts.begin(), pts.end()); return pts; }; auto pts_out = collect_sorted(mesh_out); auto pts_in = collect_sorted(mesh_in); ASSERT_EQ(pts_out.size(), pts_in.size()); for (std::size_t i = 0; i < pts_out.size(); ++i) { EXPECT_NEAR(pts_out[i][0], pts_in[i][0], 1e-10); EXPECT_NEAR(pts_out[i][1], pts_in[i][1], 1e-10); EXPECT_NEAR(pts_out[i][2], pts_in[i][2], 1e-10); } rm(path); } // ════════════════════════════════════════════════════════════════════════════ // save_mesh / load_mesh convenience wrappers // ════════════════════════════════════════════════════════════════════════════ TEST(MeshIO, SaveLoadConvenienceWrappers) { auto path = tmp_path("off"); rm(path); auto mesh_out = make_quad_strip(); EXPECT_NO_THROW(save_mesh(path, mesh_out)); ConformalMesh mesh_in; EXPECT_NO_THROW(mesh_in = load_mesh(path)); EXPECT_EQ(mesh_in.number_of_vertices(), mesh_out.number_of_vertices()); rm(path); }