444 lines
14 KiB
C++
444 lines
14 KiB
C++
#include <fesa/io/abaqus/parser.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <system_error>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
namespace {
|
|
|
|
class TemporaryDeck final {
|
|
public:
|
|
TemporaryDeck(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 Abaqus deck."};
|
|
}
|
|
}
|
|
|
|
~TemporaryDeck() {
|
|
std::error_code error;
|
|
std::filesystem::remove(path_, error);
|
|
}
|
|
|
|
TemporaryDeck(const TemporaryDeck&) = delete;
|
|
TemporaryDeck& operator=(const TemporaryDeck&) = delete;
|
|
|
|
[[nodiscard]] const std::filesystem::path& path() const noexcept {
|
|
return path_;
|
|
}
|
|
|
|
private:
|
|
std::filesystem::path path_;
|
|
};
|
|
|
|
std::filesystem::path fixture_path(std::string_view name) {
|
|
return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" /
|
|
"abaqus" / name;
|
|
}
|
|
|
|
const fesa::DeckRecord& record(
|
|
const std::vector<fesa::DeckRecord>& records,
|
|
const std::string_view keyword) {
|
|
const auto found = std::ranges::find(
|
|
records, keyword, &fesa::DeckRecord::keyword);
|
|
if (found == records.end()) {
|
|
throw std::runtime_error{"Expected deck record was not parsed."};
|
|
}
|
|
return *found;
|
|
}
|
|
|
|
TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) {
|
|
const auto path = fixture_path("minimal_cantilever.inp");
|
|
|
|
const auto result = fesa::parse_deck(path);
|
|
|
|
ASSERT_TRUE(result.deck.has_value());
|
|
EXPECT_TRUE(result.diagnostics.empty());
|
|
EXPECT_TRUE(result.deck->parts.empty());
|
|
EXPECT_FALSE(result.deck->assembly.has_value());
|
|
|
|
const auto& nodes = record(result.deck->global_records, "NODE");
|
|
ASSERT_EQ(nodes.data.size(), 2U);
|
|
EXPECT_EQ(nodes.data[0], (std::vector<std::string>{
|
|
"1", "0.0", "0.0", "0.0"}));
|
|
EXPECT_EQ(nodes.data[1], (std::vector<std::string>{
|
|
"2", "1.0", "0.0", "0.0"}));
|
|
EXPECT_EQ(nodes.source.file, path);
|
|
EXPECT_EQ(nodes.source.line, 3U);
|
|
EXPECT_EQ(nodes.source.column, 1U);
|
|
|
|
const auto& element = record(result.deck->global_records, "ELEMENT");
|
|
EXPECT_EQ(element.parameters.at("TYPE"), "B31");
|
|
EXPECT_EQ(element.parameters.at("ELSET"), "Beam");
|
|
EXPECT_EQ(element.source.line, 8U);
|
|
}
|
|
|
|
TEST(AbaqusParser, PreservesUtf8ParameterValues) {
|
|
const std::string utf8_name{
|
|
"\xEB\xB9\x94\xEB\xB6\x80\xED\x92\x88"};
|
|
const TemporaryDeck input{
|
|
"fesa-parser-utf8.inp",
|
|
"*MATERIAL, NAME=" + utf8_name + "\n"
|
|
"*ELASTIC\n"
|
|
"210000.0, 0.3\n"};
|
|
|
|
const auto result = fesa::parse_deck(input.path());
|
|
|
|
ASSERT_TRUE(result.deck.has_value());
|
|
ASSERT_TRUE(result.diagnostics.empty());
|
|
const auto& material = record(result.deck->global_records, "MATERIAL");
|
|
EXPECT_EQ(material.parameters.at("NAME"), utf8_name);
|
|
}
|
|
|
|
TEST(AbaqusParser, RejectsUnsupportedKeywordsInsteadOfIgnoringThem) {
|
|
const TemporaryDeck input{
|
|
"fesa-parser-unsupported.inp",
|
|
"** The error source must identify the keyword line.\n"
|
|
"*INCLUDE, INPUT=other.inp\n"};
|
|
|
|
const auto result = fesa::parse_deck(input.path());
|
|
|
|
EXPECT_FALSE(result.deck.has_value());
|
|
ASSERT_EQ(result.diagnostics.size(), 1U);
|
|
EXPECT_EQ(
|
|
result.diagnostics[0].stage, fesa::DiagnosticStage::syntax);
|
|
EXPECT_EQ(
|
|
result.diagnostics[0].code, "abaqus.unsupported_keyword");
|
|
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
|
EXPECT_EQ(result.diagnostics[0].source->file, input.path());
|
|
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
|
|
}
|
|
|
|
TEST(AbaqusParser, RejectsUnsupportedParametersOnSupportedKeywords) {
|
|
struct Case final {
|
|
std::string_view name;
|
|
std::string_view contents;
|
|
std::size_t line;
|
|
};
|
|
const Case cases[]{
|
|
{"node", "*NODE, EXTRA=1\n1, 0.0, 0.0, 0.0\n", 1U},
|
|
{"element", "*ELEMENT, TYPE=B31, EXTRA=1\n1, 1, 2\n", 1U},
|
|
{"part", "*PART, NAME=P, EXTRA=1\n*END PART\n", 1U},
|
|
{"end-part", "*PART, NAME=P\n*END PART, EXTRA=1\n", 2U},
|
|
{
|
|
"assembly",
|
|
"*ASSEMBLY, NAME=A, EXTRA=1\n*END ASSEMBLY\n",
|
|
1U,
|
|
},
|
|
{
|
|
"end-assembly",
|
|
"*ASSEMBLY, NAME=A\n*END ASSEMBLY, EXTRA=1\n",
|
|
2U,
|
|
},
|
|
{
|
|
"instance",
|
|
"*ASSEMBLY, NAME=A\n"
|
|
"*INSTANCE, NAME=I, PART=P, EXTRA=1\n"
|
|
"*END INSTANCE\n"
|
|
"*END ASSEMBLY\n",
|
|
2U,
|
|
},
|
|
{
|
|
"end-instance",
|
|
"*ASSEMBLY, NAME=A\n"
|
|
"*INSTANCE, NAME=I, PART=P\n"
|
|
"*END INSTANCE, EXTRA=1\n"
|
|
"*END ASSEMBLY\n",
|
|
3U,
|
|
},
|
|
{"nset", "*NSET, NSET=S, EXTRA=1\n1\n", 1U},
|
|
{"elset", "*ELSET, ELSET=S, EXTRA=1\n1\n", 1U},
|
|
{"material", "*MATERIAL, NAME=M, EXTRA=1\n", 1U},
|
|
{
|
|
"elastic",
|
|
"*MATERIAL, NAME=M\n*ELASTIC, EXTRA=1\n1.0, 0.3\n",
|
|
2U,
|
|
},
|
|
{
|
|
"beam-general-section",
|
|
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=S, "
|
|
"MATERIAL=M, EXTRA=1\n"
|
|
"1.0, 1.0, 0.0, 1.0, 1.0\n"
|
|
"0.0, 1.0, 0.0\n",
|
|
1U,
|
|
},
|
|
{
|
|
"transverse-shear-stiffness",
|
|
"*TRANSVERSE SHEAR STIFFNESS, EXTRA=1\n1.0, 1.0, 0.0\n",
|
|
1U,
|
|
},
|
|
};
|
|
|
|
for (const auto& test_case : cases) {
|
|
const TemporaryDeck input{
|
|
"fesa-parser-unsupported-parameter-" +
|
|
std::string{test_case.name} + ".inp",
|
|
test_case.contents,
|
|
};
|
|
|
|
const auto result = fesa::parse_deck(input.path());
|
|
|
|
SCOPED_TRACE(test_case.name);
|
|
EXPECT_FALSE(result.deck.has_value());
|
|
ASSERT_EQ(result.diagnostics.size(), 1U);
|
|
EXPECT_EQ(
|
|
result.diagnostics[0].code,
|
|
"abaqus.syntax.unsupported_parameter");
|
|
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
|
EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
|
|
}
|
|
}
|
|
|
|
TEST(AbaqusParser, RejectsInvalidParameterFormsAndSetScopeOptions) {
|
|
struct Case final {
|
|
std::string_view name;
|
|
std::string_view contents;
|
|
std::string_view code;
|
|
};
|
|
const Case cases[]{
|
|
{
|
|
"generate-valued",
|
|
"*NSET, NSET=S, GENERATE=YES\n1, 2, 1\n",
|
|
"abaqus.syntax.unsupported_parameter",
|
|
},
|
|
{
|
|
"generate-empty-valued",
|
|
"*NSET, NSET=S, GENERATE=\n1, 2, 1\n",
|
|
"abaqus.syntax.unsupported_parameter",
|
|
},
|
|
{
|
|
"empty-element-set",
|
|
"*ELEMENT, TYPE=B31, ELSET=\n1, 1, 2\n",
|
|
"abaqus.syntax.invalid_parameter",
|
|
},
|
|
{
|
|
"instance-on-flat-set",
|
|
"*NSET, NSET=S, INSTANCE=I\n1\n",
|
|
"abaqus.syntax.unsupported_parameter",
|
|
},
|
|
{
|
|
"instance-on-part-set",
|
|
"*PART, NAME=P\n"
|
|
"*NSET, NSET=S, INSTANCE=I\n"
|
|
"1\n"
|
|
"*END PART\n",
|
|
"abaqus.syntax.unsupported_parameter",
|
|
},
|
|
};
|
|
|
|
for (const auto& test_case : cases) {
|
|
const TemporaryDeck input{
|
|
"fesa-parser-parameter-form-" + std::string{test_case.name} +
|
|
".inp",
|
|
test_case.contents,
|
|
};
|
|
|
|
const auto result = fesa::parse_deck(input.path());
|
|
|
|
SCOPED_TRACE(test_case.name);
|
|
EXPECT_FALSE(result.deck.has_value());
|
|
ASSERT_EQ(result.diagnostics.size(), 1U);
|
|
EXPECT_EQ(result.diagnostics[0].code, test_case.code);
|
|
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
|
EXPECT_EQ(result.diagnostics[0].source->line, 1U +
|
|
(test_case.name == "instance-on-part-set" ? 1U : 0U));
|
|
}
|
|
}
|
|
|
|
TEST(AbaqusParser, RejectsModelKeywordsOutsideTheirDocumentedScopes) {
|
|
struct Case final {
|
|
std::string_view name;
|
|
std::string_view contents;
|
|
std::string_view code;
|
|
std::size_t line;
|
|
};
|
|
const Case cases[]{
|
|
{
|
|
"node-in-assembly",
|
|
"*ASSEMBLY, NAME=A\n"
|
|
"*NODE\n"
|
|
"1, 0.0, 0.0, 0.0\n"
|
|
"*END ASSEMBLY\n",
|
|
"abaqus.syntax.invalid_node_scope",
|
|
2U,
|
|
},
|
|
{
|
|
"material-in-part",
|
|
"*PART, NAME=P\n"
|
|
"*MATERIAL, NAME=M\n"
|
|
"*END PART\n",
|
|
"abaqus.syntax.invalid_material_scope",
|
|
2U,
|
|
},
|
|
{
|
|
"section-in-assembly",
|
|
"*ASSEMBLY, NAME=A\n"
|
|
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=S, MATERIAL=M\n"
|
|
"1.0, 1.0, 0.0, 1.0, 1.0\n"
|
|
"0.0, 1.0, 0.0\n"
|
|
"*END ASSEMBLY\n",
|
|
"abaqus.syntax.invalid_section_scope",
|
|
2U,
|
|
},
|
|
};
|
|
|
|
for (const auto& test_case : cases) {
|
|
const TemporaryDeck input{
|
|
"fesa-parser-invalid-scope-" + std::string{test_case.name} +
|
|
".inp",
|
|
test_case.contents,
|
|
};
|
|
|
|
const auto result = fesa::parse_deck(input.path());
|
|
|
|
SCOPED_TRACE(test_case.name);
|
|
EXPECT_FALSE(result.deck.has_value());
|
|
ASSERT_EQ(result.diagnostics.size(), 1U);
|
|
EXPECT_EQ(result.diagnostics[0].code, test_case.code);
|
|
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
|
EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
|
|
}
|
|
}
|
|
|
|
TEST(AbaqusParser, RejectsDataRowsOnMaterial) {
|
|
const TemporaryDeck input{
|
|
"fesa-parser-material-data.inp",
|
|
"*MATERIAL, NAME=Steel\n"
|
|
"unexpected data\n"};
|
|
|
|
const auto result = fesa::parse_deck(input.path());
|
|
|
|
EXPECT_FALSE(result.deck.has_value());
|
|
ASSERT_EQ(result.diagnostics.size(), 1U);
|
|
EXPECT_EQ(
|
|
result.diagnostics[0].code,
|
|
"abaqus.syntax.data_without_keyword");
|
|
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
|
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
|
|
}
|
|
|
|
TEST(ScopedDeck, PreservesPartAssemblyAndInstanceScopes) {
|
|
const auto path =
|
|
fixture_path("minimal_part_instance_cantilever.inp");
|
|
|
|
const auto result = fesa::parse_deck(path);
|
|
|
|
ASSERT_TRUE(result.deck.has_value());
|
|
EXPECT_TRUE(result.diagnostics.empty());
|
|
ASSERT_EQ(result.deck->parts.size(), 1U);
|
|
EXPECT_EQ(result.deck->parts[0].name, "BeamPart");
|
|
EXPECT_EQ(result.deck->parts[0].source.file, path);
|
|
EXPECT_EQ(result.deck->parts[0].source.line, 2U);
|
|
EXPECT_EQ(record(result.deck->parts[0].records, "NODE").source.line, 3U);
|
|
|
|
ASSERT_TRUE(result.deck->assembly.has_value());
|
|
EXPECT_EQ(result.deck->assembly->name, "RootAssembly");
|
|
EXPECT_EQ(result.deck->assembly->source.line, 19U);
|
|
ASSERT_EQ(result.deck->assembly->instances.size(), 1U);
|
|
EXPECT_EQ(result.deck->assembly->instances[0].name, "Beam-1");
|
|
EXPECT_EQ(result.deck->assembly->instances[0].part_name, "BeamPart");
|
|
EXPECT_EQ(result.deck->assembly->instances[0].source.line, 20U);
|
|
EXPECT_TRUE(
|
|
result.deck->assembly->instances[0].transform_data.empty());
|
|
EXPECT_EQ(
|
|
record(result.deck->assembly->records, "NSET")
|
|
.parameters.at("INSTANCE"),
|
|
"Beam-1");
|
|
|
|
EXPECT_NE(
|
|
std::ranges::find(
|
|
result.deck->global_records,
|
|
"MATERIAL",
|
|
&fesa::DeckRecord::keyword),
|
|
result.deck->global_records.end());
|
|
}
|
|
|
|
TEST(ScopedDeck, PreservesInstanceTransformDataWithoutApplyingIt) {
|
|
const TemporaryDeck input{
|
|
"fesa-parser-transform.inp",
|
|
"*PART, NAME=BeamPart\n"
|
|
"*END PART\n"
|
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
|
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
|
"1.0, 2.0, 3.0\n"
|
|
"0.0, 0.0, 1.0, 90.0\n"
|
|
"*END INSTANCE\n"
|
|
"*END ASSEMBLY\n"};
|
|
|
|
const auto result = fesa::parse_deck(input.path());
|
|
|
|
ASSERT_TRUE(result.deck.has_value());
|
|
ASSERT_TRUE(result.deck->assembly.has_value());
|
|
ASSERT_EQ(result.deck->assembly->instances.size(), 1U);
|
|
EXPECT_EQ(
|
|
result.deck->assembly->instances[0].transform_data,
|
|
(std::vector<std::vector<std::string>>{
|
|
{"1.0", "2.0", "3.0"},
|
|
{"0.0", "0.0", "1.0", "90.0"}}));
|
|
}
|
|
|
|
TEST(ScopedDeck, ReportsUnexpectedAndUnclosedScopeTerminators) {
|
|
struct Case final {
|
|
std::string_view name;
|
|
std::string_view contents;
|
|
std::string_view code;
|
|
std::size_t line;
|
|
};
|
|
const Case cases[]{
|
|
{
|
|
"fesa-parser-unexpected-end-part.inp",
|
|
"*END PART\n",
|
|
"abaqus.syntax.unexpected_end_part",
|
|
1U,
|
|
},
|
|
{
|
|
"fesa-parser-unclosed-part.inp",
|
|
"*PART, NAME=BeamPart\n"
|
|
"*NODE\n"
|
|
"1, 0.0, 0.0, 0.0\n",
|
|
"abaqus.syntax.unclosed_part",
|
|
1U,
|
|
},
|
|
{
|
|
"fesa-parser-unexpected-end-instance.inp",
|
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
|
"*END INSTANCE\n",
|
|
"abaqus.syntax.unexpected_end_instance",
|
|
2U,
|
|
},
|
|
{
|
|
"fesa-parser-unclosed-assembly.inp",
|
|
"*ASSEMBLY, NAME=RootAssembly\n",
|
|
"abaqus.syntax.unclosed_assembly",
|
|
1U,
|
|
},
|
|
};
|
|
|
|
for (const auto& test_case : cases) {
|
|
const TemporaryDeck input{test_case.name, test_case.contents};
|
|
|
|
const auto result = fesa::parse_deck(input.path());
|
|
|
|
SCOPED_TRACE(test_case.name);
|
|
EXPECT_FALSE(result.deck.has_value());
|
|
ASSERT_EQ(result.diagnostics.size(), 1U);
|
|
EXPECT_EQ(result.diagnostics[0].code, test_case.code);
|
|
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
|
EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
|
|
}
|
|
}
|
|
|
|
} // namespace
|