|
|
|
@@ -0,0 +1,235 @@
|
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
#include <stdexcept>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <string_view>
|
|
|
|
|
#include <system_error>
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
#include <fesa/validation/reference_csv.hpp>
|
|
|
|
|
|
|
|
|
|
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<std::streamsize>(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<fesa::Diagnostic>& 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<double>{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<double>{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<double>{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<double>{42.5}));
|
|
|
|
|
EXPECT_EQ(result.rows.back().values, (std::vector<double>{-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<double>{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
|