295 lines
9.7 KiB
C++
295 lines
9.7 KiB
C++
#include <fesa/io/abaqus/parser.hpp>
|
|
#include <fesa/io/abaqus/semantic_mapper.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <optional>
|
|
#include <ranges>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
namespace {
|
|
|
|
struct ContractCase final {
|
|
std::string id;
|
|
bool valid;
|
|
std::filesystem::path fixture;
|
|
std::optional<fesa::DiagnosticStage> expected_stage;
|
|
std::string expected_code;
|
|
std::size_t expected_line;
|
|
std::optional<std::size_t> node_count;
|
|
std::optional<std::size_t> element_count;
|
|
std::optional<std::size_t> node_set_count;
|
|
std::optional<std::size_t> element_set_count;
|
|
std::optional<std::size_t> prescribed_dof_count;
|
|
std::optional<std::size_t> nodal_load_count;
|
|
std::string shear_source;
|
|
std::optional<double> shear_area_y;
|
|
std::optional<double> shear_area_z;
|
|
std::string checked_node_set;
|
|
std::string checked_element_set;
|
|
};
|
|
|
|
struct ContractOutcome final {
|
|
std::optional<fesa::Domain> domain;
|
|
std::vector<fesa::Diagnostic> diagnostics;
|
|
};
|
|
|
|
std::vector<std::string> split(
|
|
const std::string_view text,
|
|
const char delimiter) {
|
|
std::vector<std::string> fields;
|
|
std::size_t first = 0;
|
|
while (true) {
|
|
const std::size_t next = text.find(delimiter, first);
|
|
fields.emplace_back(
|
|
text.substr(
|
|
first,
|
|
next == std::string_view::npos
|
|
? std::string_view::npos
|
|
: next - first));
|
|
if (next == std::string_view::npos) {
|
|
break;
|
|
}
|
|
first = next + 1U;
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
std::optional<std::size_t> optional_size(const std::string& field) {
|
|
if (field == "-") {
|
|
return std::nullopt;
|
|
}
|
|
return static_cast<std::size_t>(std::stoull(field));
|
|
}
|
|
|
|
std::optional<double> optional_real(const std::string& field) {
|
|
if (field == "-") {
|
|
return std::nullopt;
|
|
}
|
|
return std::stod(field);
|
|
}
|
|
|
|
std::optional<fesa::DiagnosticStage> optional_stage(
|
|
const std::string& field) {
|
|
if (field == "-") {
|
|
return std::nullopt;
|
|
}
|
|
if (field == "io") {
|
|
return fesa::DiagnosticStage::io;
|
|
}
|
|
if (field == "syntax") {
|
|
return fesa::DiagnosticStage::syntax;
|
|
}
|
|
if (field == "semantic") {
|
|
return fesa::DiagnosticStage::semantic;
|
|
}
|
|
if (field == "model") {
|
|
return fesa::DiagnosticStage::model;
|
|
}
|
|
throw std::runtime_error{"Unknown diagnostic stage in contract manifest."};
|
|
}
|
|
|
|
std::vector<ContractCase> load_contract_cases() {
|
|
const std::filesystem::path path =
|
|
std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" / "abaqus" /
|
|
"contract.tsv";
|
|
std::ifstream input{path, std::ios::binary};
|
|
if (!input) {
|
|
throw std::runtime_error{"Unable to open Abaqus contract manifest."};
|
|
}
|
|
|
|
std::vector<ContractCase> cases;
|
|
std::string line;
|
|
while (std::getline(input, line)) {
|
|
if (line.empty() || line.front() == '#') {
|
|
continue;
|
|
}
|
|
const std::vector<std::string> fields = split(line, '\t');
|
|
if (fields.size() != 17U) {
|
|
throw std::runtime_error{
|
|
"Abaqus contract manifest row must contain 17 fields."};
|
|
}
|
|
const bool valid = fields[1] == "valid";
|
|
if (!valid && fields[1] != "invalid") {
|
|
throw std::runtime_error{
|
|
"Abaqus contract outcome must be valid or invalid."};
|
|
}
|
|
cases.push_back({
|
|
fields[0],
|
|
valid,
|
|
fields[2],
|
|
optional_stage(fields[3]),
|
|
fields[4] == "-" ? std::string{} : fields[4],
|
|
static_cast<std::size_t>(std::stoull(fields[5])),
|
|
optional_size(fields[6]),
|
|
optional_size(fields[7]),
|
|
optional_size(fields[8]),
|
|
optional_size(fields[9]),
|
|
optional_size(fields[10]),
|
|
optional_size(fields[11]),
|
|
fields[12],
|
|
optional_real(fields[13]),
|
|
optional_real(fields[14]),
|
|
fields[15],
|
|
fields[16],
|
|
});
|
|
}
|
|
if (cases.empty()) {
|
|
throw std::runtime_error{"Abaqus contract manifest is empty."};
|
|
}
|
|
return cases;
|
|
}
|
|
|
|
ContractOutcome parse_and_map(const std::filesystem::path& path) {
|
|
auto parsed = fesa::parse_deck(path);
|
|
if (!parsed.deck.has_value()) {
|
|
return {std::nullopt, std::move(parsed.diagnostics)};
|
|
}
|
|
auto mapped = fesa::map_deck_to_domain(*parsed.deck);
|
|
return {std::move(mapped.domain), std::move(mapped.diagnostics)};
|
|
}
|
|
|
|
std::string diagnostics_text(
|
|
const std::vector<fesa::Diagnostic>& diagnostics) {
|
|
std::ostringstream output;
|
|
for (const auto& diagnostic : diagnostics) {
|
|
output << diagnostic.code;
|
|
if (diagnostic.source.has_value()) {
|
|
output << '@' << diagnostic.source->line;
|
|
}
|
|
output << '\n';
|
|
}
|
|
return output.str();
|
|
}
|
|
|
|
std::pair<std::string, std::vector<std::int64_t>> expected_set(
|
|
const std::string& field) {
|
|
const std::size_t colon = field.find(':');
|
|
if (colon == std::string::npos) {
|
|
throw std::runtime_error{"Set check must use name:label,... format."};
|
|
}
|
|
std::vector<std::int64_t> labels;
|
|
for (const std::string& label :
|
|
split(std::string_view{field}.substr(colon + 1U), ',')) {
|
|
labels.push_back(std::stoll(label));
|
|
}
|
|
return {field.substr(0, colon), std::move(labels)};
|
|
}
|
|
|
|
void expect_node_set(
|
|
const fesa::Domain& domain,
|
|
const std::string& field) {
|
|
if (field == "-") {
|
|
return;
|
|
}
|
|
const auto [name, expected_labels] = expected_set(field);
|
|
const auto found = std::ranges::find(
|
|
domain.node_sets(), name, &fesa::NodeSet::name);
|
|
ASSERT_NE(found, domain.node_sets().end());
|
|
|
|
std::vector<std::int64_t> actual_labels;
|
|
for (const fesa::NodeId member : found->members) {
|
|
actual_labels.push_back(domain.node(member).origin.local_label);
|
|
}
|
|
std::ranges::sort(actual_labels);
|
|
EXPECT_EQ(actual_labels, expected_labels);
|
|
}
|
|
|
|
void expect_element_set(
|
|
const fesa::Domain& domain,
|
|
const std::string& field) {
|
|
if (field == "-") {
|
|
return;
|
|
}
|
|
const auto [name, expected_labels] = expected_set(field);
|
|
const auto found = std::ranges::find(
|
|
domain.element_sets(), name, &fesa::ElementSet::name);
|
|
ASSERT_NE(found, domain.element_sets().end());
|
|
|
|
std::vector<std::int64_t> actual_labels;
|
|
for (const fesa::ElementId member : found->members) {
|
|
const auto element = std::ranges::find(
|
|
domain.beam_elements(), member, &fesa::BeamElement::id);
|
|
ASSERT_NE(element, domain.beam_elements().end());
|
|
actual_labels.push_back(element->origin.local_label);
|
|
}
|
|
std::ranges::sort(actual_labels);
|
|
EXPECT_EQ(actual_labels, expected_labels);
|
|
}
|
|
|
|
class AbaqusInputContractTest
|
|
: public testing::TestWithParam<ContractCase> {};
|
|
|
|
TEST_P(AbaqusInputContractTest, FixtureMatchesNormativeContract) {
|
|
const ContractCase& contract = GetParam();
|
|
const std::filesystem::path path =
|
|
std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" / "abaqus" /
|
|
contract.fixture;
|
|
SCOPED_TRACE(contract.id);
|
|
ASSERT_TRUE(std::filesystem::is_regular_file(path));
|
|
|
|
const ContractOutcome outcome = parse_and_map(path);
|
|
if (!contract.valid) {
|
|
EXPECT_FALSE(outcome.domain.has_value());
|
|
ASSERT_TRUE(contract.expected_stage.has_value());
|
|
const auto diagnostic = std::ranges::find_if(
|
|
outcome.diagnostics,
|
|
[&contract](const fesa::Diagnostic& candidate) {
|
|
return candidate.stage == *contract.expected_stage &&
|
|
candidate.code == contract.expected_code &&
|
|
candidate.source.has_value() &&
|
|
candidate.source->line == contract.expected_line;
|
|
});
|
|
EXPECT_NE(diagnostic, outcome.diagnostics.end())
|
|
<< "Expected " << contract.expected_code << '@'
|
|
<< contract.expected_line << " but received:\n"
|
|
<< diagnostics_text(outcome.diagnostics);
|
|
return;
|
|
}
|
|
|
|
ASSERT_TRUE(outcome.domain.has_value())
|
|
<< diagnostics_text(outcome.diagnostics);
|
|
ASSERT_TRUE(outcome.diagnostics.empty())
|
|
<< diagnostics_text(outcome.diagnostics);
|
|
const fesa::Domain& domain = *outcome.domain;
|
|
EXPECT_EQ(domain.nodes().size(), *contract.node_count);
|
|
EXPECT_EQ(domain.beam_elements().size(), *contract.element_count);
|
|
EXPECT_EQ(domain.node_sets().size(), *contract.node_set_count);
|
|
EXPECT_EQ(domain.element_sets().size(), *contract.element_set_count);
|
|
EXPECT_EQ(
|
|
domain.step().prescribed_dofs.size(),
|
|
*contract.prescribed_dof_count);
|
|
EXPECT_EQ(domain.step().nodal_loads.size(), *contract.nodal_load_count);
|
|
|
|
ASSERT_EQ(domain.sections().size(), 1U);
|
|
const fesa::BeamSection& section = domain.sections().front();
|
|
const auto expected_source = contract.shear_source == "input"
|
|
? fesa::ShearPropertySource::input
|
|
: fesa::ShearPropertySource::phase1_default;
|
|
EXPECT_EQ(section.shear_source, expected_source);
|
|
EXPECT_NEAR(section.shear_area_y, *contract.shear_area_y, 1.0e-12);
|
|
EXPECT_NEAR(section.shear_area_z, *contract.shear_area_z, 1.0e-12);
|
|
expect_node_set(domain, contract.checked_node_set);
|
|
expect_element_set(domain, contract.checked_element_set);
|
|
}
|
|
|
|
INSTANTIATE_TEST_SUITE_P(
|
|
AbaqusInputContract,
|
|
AbaqusInputContractTest,
|
|
testing::ValuesIn(load_contract_cases()),
|
|
[](const testing::TestParamInfo<ContractCase>& info) {
|
|
return info.param.id;
|
|
});
|
|
|
|
} // namespace
|