#include #include #include #include #include #include #include #include #include #include namespace { class TemporaryCsv final { public: TemporaryCsv(std::string_view name, std::string_view contents) : path_{std::filesystem::path{testing::TempDir()} / name} { std::ofstream output{path_, std::ios::binary}; output.write( contents.data(), static_cast(contents.size())); if (!output) { throw std::runtime_error{"Failed to write temporary reference CSV."}; } } ~TemporaryCsv() { std::error_code error; std::filesystem::remove(path_, error); } TemporaryCsv(const TemporaryCsv&) = delete; TemporaryCsv& operator=(const TemporaryCsv&) = delete; [[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; } private: std::filesystem::path path_; }; std::filesystem::path fixture_path(const std::string_view name) { return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" / "reference" / name; } std::filesystem::path supplied_reference_path(const std::string_view name) { return std::filesystem::path{FESA_TEST_SOURCE_DIR}.parent_path() / "reference" / "cantilever beam" / name; } bool has_diagnostic( const std::vector& diagnostics, const std::string_view code) { return std::ranges::any_of( diagnostics, [code](const fesa::Diagnostic& diagnostic) { return diagnostic.code == code; }); } TEST(ReferenceCsv, ReadsSuppliedDisplacementsWithWhitespaceHeader) { const auto result = fesa::read_reference_csv( fesa::ReferenceQuantity::displacement, supplied_reference_path("cantilever beam displacements.csv"), "Part-1-1"); ASSERT_TRUE(result.diagnostics.empty()); ASSERT_EQ(result.rows.size(), 11U); EXPECT_EQ( result.rows.front().quantity, fesa::ReferenceQuantity::displacement); EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1"); EXPECT_EQ(result.rows.front().position.entity_label, 1); EXPECT_FALSE(result.rows.front().position.end_node_label.has_value()); EXPECT_EQ( result.rows.front().values, (std::vector{0.0, 0.0, -1.0e-32, 0.0, 1.0e-31, 0.0})); EXPECT_EQ(result.rows.back().position.entity_label, 11); EXPECT_EQ(result.rows.back().values[2], -1.92e-4); } TEST(ReferenceCsv, ReadsSuppliedReactionsWithWhitespaceHeader) { const auto result = fesa::read_reference_csv( fesa::ReferenceQuantity::reaction, supplied_reference_path("cantilever beam reactions.csv"), "Part-1-1"); ASSERT_TRUE(result.diagnostics.empty()); ASSERT_EQ(result.rows.size(), 11U); EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1"); EXPECT_EQ(result.rows.front().position.entity_label, 1); EXPECT_FALSE(result.rows.front().position.end_node_label.has_value()); EXPECT_EQ( result.rows.front().values, (std::vector{0.0, 0.0, 1.0e4, 0.0, -1.0e5, 0.0})); } TEST(InternalForceCsv, MapsAllSixComponentsAndElementEndPosition) { const auto result = fesa::read_reference_csv( fesa::ReferenceQuantity::internal_force, fixture_path("internalforces.csv"), "unused-request-name"); ASSERT_TRUE(result.diagnostics.empty()); ASSERT_EQ(result.rows.size(), 2U); EXPECT_EQ( result.rows.front().quantity, fesa::ReferenceQuantity::internal_force); EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1"); EXPECT_EQ(result.rows.front().position.entity_label, 501); ASSERT_TRUE(result.rows.front().position.end_node_label.has_value()); EXPECT_EQ(*result.rows.front().position.end_node_label, 101); EXPECT_EQ( result.rows.front().values, (std::vector{1.25, -2.5, 3.75, -4.0, 5.5, -6.25})); } TEST(StressCsv, FillsOmittedInstanceAndReadsCentroidStress) { const auto result = fesa::read_reference_csv( fesa::ReferenceQuantity::centroid_stress, fixture_path("stresses.csv"), "Part-1-1"); ASSERT_TRUE(result.diagnostics.empty()); ASSERT_EQ(result.rows.size(), 2U); EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1"); EXPECT_EQ(result.rows.front().position.entity_label, 501); ASSERT_TRUE(result.rows.front().position.end_node_label.has_value()); EXPECT_EQ(*result.rows.front().position.end_node_label, 101); EXPECT_EQ(result.rows.front().values, (std::vector{42.5})); EXPECT_EQ(result.rows.back().values, (std::vector{-17.25})); } TEST(ReferenceCsv, AcceptsUtf8BomOnlyAtTheStartAndTrimsValues) { const TemporaryCsv input{ "fesa-reference-bom.csv", "\xEF\xBB\xBF Part Instance Name , Node Label , U-U1 , U-U2 , " "U-U3 , UR-UR1 , UR-UR2 , UR-UR3\n" " Beam-1 , 7 , 1 , 2 , 3 , 4 , 5 , 6 \n"}; const auto result = fesa::read_reference_csv( fesa::ReferenceQuantity::displacement, input.path(), "unused"); ASSERT_TRUE(result.diagnostics.empty()); ASSERT_EQ(result.rows.size(), 1U); EXPECT_EQ(result.rows[0].position.instance_name, "Beam-1"); EXPECT_EQ(result.rows[0].position.entity_label, 7); EXPECT_EQ( result.rows[0].values, (std::vector{1.0, 2.0, 3.0, 4.0, 5.0, 6.0})); } TEST(ReferenceCsv, DiagnosesMissingRequiredColumn) { const TemporaryCsv input{ "fesa-reference-missing-column.csv", "Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,Unexpected\n" "1,0,0,0,0,0,0\n"}; const auto result = fesa::read_reference_csv( fesa::ReferenceQuantity::displacement, input.path(), "Part-1-1"); EXPECT_TRUE(result.rows.empty()); ASSERT_TRUE(has_diagnostic( result.diagnostics, "validation.reference_csv_missing_column")); ASSERT_TRUE(result.diagnostics.front().source.has_value()); EXPECT_EQ(result.diagnostics.front().source->line, 1U); } TEST(ReferenceCsv, DiagnosesDuplicateResultPosition) { const TemporaryCsv input{ "fesa-reference-duplicate.csv", "Element Label,Node Label,Sxx\n" "8,2,10\n" "8,2,11\n"}; const auto result = fesa::read_reference_csv( fesa::ReferenceQuantity::centroid_stress, input.path(), "Part-1-1"); EXPECT_TRUE(result.rows.empty()); ASSERT_TRUE(has_diagnostic( result.diagnostics, "validation.reference_csv_duplicate_row")); ASSERT_TRUE(result.diagnostics.front().source.has_value()); EXPECT_EQ(result.diagnostics.front().source->line, 3U); } TEST(ReferenceCsv, DiagnosesInvalidAndNonfiniteNumbers) { const TemporaryCsv invalid{ "fesa-reference-invalid-number.csv", "Node Label,RF-RF1,RF-RF2,RF-RF3,RM-RM1,RM-RM2,RM-RM3\n" "1,0,0,not-a-number,0,0,0\n"}; const TemporaryCsv nonfinite{ "fesa-reference-nonfinite-number.csv", "Element Label,Node Label,Sxx\n" "8,2,nan\n"}; const auto invalid_result = fesa::read_reference_csv( fesa::ReferenceQuantity::reaction, invalid.path(), "Part-1-1"); const auto nonfinite_result = fesa::read_reference_csv( fesa::ReferenceQuantity::centroid_stress, nonfinite.path(), "Part-1-1"); EXPECT_TRUE(invalid_result.rows.empty()); EXPECT_TRUE(nonfinite_result.rows.empty()); EXPECT_TRUE(has_diagnostic( invalid_result.diagnostics, "validation.reference_csv_invalid_number")); EXPECT_TRUE(has_diagnostic( nonfinite_result.diagnostics, "validation.reference_csv_invalid_number")); } TEST(ReferenceCsv, RejectsUtf8BomOutsideTheFileStart) { const TemporaryCsv input{ "fesa-reference-misplaced-bom.csv", "Node Label,\xEF\xBB\xBF U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3\n" "1,0,0,0,0,0,0\n"}; const auto result = fesa::read_reference_csv( fesa::ReferenceQuantity::displacement, input.path(), "Part-1-1"); EXPECT_TRUE(result.rows.empty()); EXPECT_TRUE(has_diagnostic( result.diagnostics, "validation.reference_csv_invalid_encoding")); } } // namespace