Files
FESA/tests/unit/io/abaqus/input_contract_test.cpp
T

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