#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { struct ContractCase final { std::string id; bool valid; std::filesystem::path fixture; std::optional expected_stage; std::string expected_code; std::size_t expected_line; std::optional node_count; std::optional element_count; std::optional node_set_count; std::optional element_set_count; std::optional prescribed_dof_count; std::optional nodal_load_count; std::string shear_source; std::optional shear_area_y; std::optional shear_area_z; std::string checked_node_set; std::string checked_element_set; }; struct ContractOutcome final { std::optional domain; std::vector diagnostics; }; std::vector split( const std::string_view text, const char delimiter) { std::vector 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 optional_size(const std::string& field) { if (field == "-") { return std::nullopt; } return static_cast(std::stoull(field)); } std::optional optional_real(const std::string& field) { if (field == "-") { return std::nullopt; } return std::stod(field); } std::optional 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 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 cases; std::string line; while (std::getline(input, line)) { if (line.empty() || line.front() == '#') { continue; } const std::vector 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::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& 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> 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 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 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 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 {}; 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& info) { return info.param.id; }); } // namespace