fix(input): enforce strict Abaqus subset contract

This commit is contained in:
KOKO\Mimi
2026-08-01 03:48:29 +09:00
parent 9269847c83
commit af886f3f90
25 changed files with 722 additions and 34 deletions
@@ -173,6 +173,128 @@ TEST(DeckToDomain, NormalizesFlatAndSingleInstanceDecksEquivalently) {
fesa::ShearPropertySource::phase1_default);
}
TEST(DeckToDomain, RequiresExactNodeAndB31DataWidthsAndNonemptyRecords) {
struct Case final {
std::string_view name;
std::string_view mesh;
std::string_view code;
};
const Case cases[]{
{
"node-surplus-field",
"*NODE\n1, 0.0, 0.0, 0.0, 9.0\n"
"2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2\n",
"abaqus.semantic.invalid_node_data",
},
{
"node-empty",
"*NODE\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2\n",
"abaqus.semantic.invalid_node_data",
},
{
"element-surplus-field",
"*NODE\n1, 0.0, 0.0, 0.0\n2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2, 3\n",
"abaqus.semantic.invalid_element_data",
},
{
"element-empty",
"*NODE\n1, 0.0, 0.0, 0.0\n2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n",
"abaqus.semantic.invalid_element_data",
},
};
for (const auto& test_case : cases) {
const TemporaryDeck input{
"fesa-exact-mesh-data-" + std::string{test_case.name} + ".inp",
std::string{test_case.mesh} +
"*ELSET, ELSET=Beam\n1\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n210000.0, 0.3\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, "
"MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n0.0, 1.0, 0.0\n"
"*STEP\n*STATIC\n*END STEP\n",
};
const auto result = parse_and_map(input.path());
SCOPED_TRACE(test_case.name);
EXPECT_FALSE(result.domain.has_value());
EXPECT_TRUE(has_diagnostic(result, test_case.code));
}
}
TEST(DeckToDomain, ReportsMeshDataErrorsAtTheOffendingRows) {
const TemporaryDeck invalid_coordinate{
"fesa-node-coordinate-source.inp",
"*NODE\n"
"1, invalid, 0.0, 0.0\n"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
const TemporaryDeck missing_node{
"fesa-element-node-source.inp",
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
"1, 1, 2\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n"
"210000.0, 0.3\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
const auto coordinate_result = parse_and_map(invalid_coordinate.path());
const auto missing_node_result = parse_and_map(missing_node.path());
const fesa::Diagnostic* coordinate =
find_diagnostic(coordinate_result, "abaqus.semantic.invalid_number");
ASSERT_NE(coordinate, nullptr);
ASSERT_TRUE(coordinate->source.has_value());
EXPECT_EQ(coordinate->source->line, 2U);
const fesa::Diagnostic* node =
find_diagnostic(missing_node_result, "abaqus.semantic.missing_node");
ASSERT_NE(node, nullptr);
ASSERT_TRUE(node->source.has_value());
EXPECT_EQ(node->source->line, 4U);
}
TEST(DeckToDomain, AcceptsCaseInsensitiveElementAndSectionValues) {
const TemporaryDeck input{
"fesa-case-insensitive-enums.inp",
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=b31, ELSET=Beam\n"
"1, 1, 2\n"
"*ELSET, ELSET=Beam\n"
"1\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n"
"210000.0, 0.3\n"
"*BEAM GENERAL SECTION, SECTION=general, ELSET=Beam, MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
ASSERT_TRUE(result.domain.has_value());
EXPECT_TRUE(result.diagnostics.empty());
EXPECT_EQ(result.domain->beam_elements().size(), 1U);
}
TEST(ActiveInstance, ExcludesPartsNotReferencedByTheInstance) {
const TemporaryDeck input{
"fesa-active-instance.inp",
@@ -406,6 +528,42 @@ TEST(Boundary, CanonicalizesIdenticalGlobalAndStepPrescriptions) {
}
}
TEST(Boundary, HierarchicalTargetsRequireAssemblySets) {
const TemporaryDeck input{
"fesa-hierarchical-part-set-target.inp",
"*PART, NAME=BeamPart\n"
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
"1, 1, 2\n"
"*NSET, NSET=Fixed\n"
"1\n"
"*ELSET, ELSET=Beam\n"
"1\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n"
"*END PART\n"
"*ASSEMBLY, NAME=RootAssembly\n"
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n"
"210000.0, 0.3\n"
"*STEP\n"
"*STATIC\n"
"*BOUNDARY\n"
"Fixed, 1, 6\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
EXPECT_FALSE(result.domain.has_value());
EXPECT_TRUE(has_diagnostic(result, "abaqus.semantic.missing_node_target"));
}
TEST(Boundary, ReportsConflictAtTheConflictingDataRow) {
const auto result =
parse_and_map(fixture_path("invalid/boundary_conflict.inp"));