feat(domain-and-input-skeleton): step 2 — abaqus-scoped-syntax-parser
This commit is contained in:
@@ -27,6 +27,7 @@ include(cmake/FesaDependencies.cmake)
|
||||
|
||||
add_library(fesa_core STATIC
|
||||
src/fesa/core/version.cpp
|
||||
src/fesa/io/abaqus/parser.cpp
|
||||
src/fesa/model/domain.cpp
|
||||
src/fesa/model/domain_builder.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/source_location.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
struct DeckRecord final {
|
||||
std::string keyword;
|
||||
std::map<std::string, std::string, std::less<>> parameters;
|
||||
std::vector<std::vector<std::string>> data;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
struct ParsedPart final {
|
||||
std::string name;
|
||||
std::vector<DeckRecord> records;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
struct ParsedInstance final {
|
||||
std::string name;
|
||||
std::string part_name;
|
||||
std::vector<std::vector<std::string>> transform_data;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
struct ParsedAssembly final {
|
||||
std::string name;
|
||||
std::vector<ParsedInstance> instances;
|
||||
std::vector<DeckRecord> records;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
struct ParsedDeck final {
|
||||
std::vector<DeckRecord> global_records;
|
||||
std::vector<ParsedPart> parts;
|
||||
std::optional<ParsedAssembly> assembly;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
#include <fesa/io/abaqus/deck_record.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
struct ParseDeckResult final {
|
||||
std::optional<ParsedDeck> deck;
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
};
|
||||
|
||||
[[nodiscard]] ParseDeckResult parse_deck(
|
||||
const std::filesystem::path& path);
|
||||
|
||||
} // namespace fesa
|
||||
+1
-1
@@ -42,4 +42,4 @@
|
||||
"status": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
#include <fesa/io/abaqus/parser.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
enum class Scope { global, part, assembly, instance };
|
||||
|
||||
struct KeywordLine final {
|
||||
std::string keyword;
|
||||
std::map<std::string, std::string, std::less<>> parameters;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
std::string_view trim(const std::string_view value) {
|
||||
constexpr std::string_view whitespace{" \t\f\v\r\n"};
|
||||
const std::size_t first = value.find_first_not_of(whitespace);
|
||||
if (first == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
const std::size_t last = value.find_last_not_of(whitespace);
|
||||
return value.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
std::string uppercase_ascii(std::string value) {
|
||||
std::ranges::transform(value, value.begin(), [](const char character) {
|
||||
if (character >= 'a' && character <= 'z') {
|
||||
return static_cast<char>(character - 'a' + 'A');
|
||||
}
|
||||
return character;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::vector<std::string> split_fields(const std::string_view value) {
|
||||
std::vector<std::string> fields;
|
||||
std::size_t first = 0;
|
||||
while (true) {
|
||||
const std::size_t comma = value.find(',', first);
|
||||
const std::string_view field = comma == std::string_view::npos
|
||||
? value.substr(first)
|
||||
: value.substr(first, comma - first);
|
||||
fields.emplace_back(trim(field));
|
||||
if (comma == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
first = comma + 1;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
ParseDeckResult failure(
|
||||
const DiagnosticStage stage,
|
||||
std::string code,
|
||||
std::string message,
|
||||
std::optional<SourceLocation> source) {
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
diagnostics.push_back({
|
||||
stage,
|
||||
Severity::error,
|
||||
std::move(code),
|
||||
std::move(message),
|
||||
std::move(source),
|
||||
});
|
||||
return {std::nullopt, std::move(diagnostics)};
|
||||
}
|
||||
|
||||
ParseDeckResult syntax_failure(
|
||||
std::string code,
|
||||
std::string message,
|
||||
SourceLocation source) {
|
||||
return failure(
|
||||
DiagnosticStage::syntax,
|
||||
std::move(code),
|
||||
std::move(message),
|
||||
std::move(source));
|
||||
}
|
||||
|
||||
bool is_supported_record(const std::string_view keyword) {
|
||||
constexpr std::array supported{
|
||||
std::string_view{"BEAM GENERAL SECTION"},
|
||||
std::string_view{"BOUNDARY"},
|
||||
std::string_view{"CLOAD"},
|
||||
std::string_view{"ELASTIC"},
|
||||
std::string_view{"ELEMENT"},
|
||||
std::string_view{"ELSET"},
|
||||
std::string_view{"END STEP"},
|
||||
std::string_view{"MATERIAL"},
|
||||
std::string_view{"NODE"},
|
||||
std::string_view{"NSET"},
|
||||
std::string_view{"STATIC"},
|
||||
std::string_view{"STEP"},
|
||||
std::string_view{"TRANSVERSE SHEAR STIFFNESS"},
|
||||
};
|
||||
return std::ranges::find(supported, keyword) != supported.end();
|
||||
}
|
||||
|
||||
std::optional<KeywordLine> parse_keyword_line(
|
||||
const std::string_view line,
|
||||
const SourceLocation& source,
|
||||
ParseDeckResult& error) {
|
||||
const std::vector<std::string> fields = split_fields(line.substr(1));
|
||||
if (fields.empty() || fields[0].empty()) {
|
||||
error = syntax_failure(
|
||||
"abaqus.syntax.empty_keyword",
|
||||
"Abaqus keyword name is empty.",
|
||||
source);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
KeywordLine parsed{
|
||||
uppercase_ascii(fields[0]),
|
||||
{},
|
||||
source,
|
||||
};
|
||||
for (std::size_t index = 1; index < fields.size(); ++index) {
|
||||
const std::string_view field = fields[index];
|
||||
if (field.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t equals = field.find('=');
|
||||
const std::string_view key_text =
|
||||
trim(field.substr(0, equals));
|
||||
const std::string_view value_text =
|
||||
equals == std::string_view::npos
|
||||
? std::string_view{}
|
||||
: trim(field.substr(equals + 1));
|
||||
if (key_text.empty()) {
|
||||
error = syntax_failure(
|
||||
"abaqus.syntax.invalid_parameter",
|
||||
"Abaqus keyword parameter name is empty.",
|
||||
source);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::string key = uppercase_ascii(std::string{key_text});
|
||||
if (!parsed.parameters.emplace(key, value_text).second) {
|
||||
error = syntax_failure(
|
||||
"abaqus.syntax.duplicate_parameter",
|
||||
"Abaqus keyword parameter '" + key +
|
||||
"' is specified more than once.",
|
||||
source);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const std::string* parameter(
|
||||
const KeywordLine& keyword,
|
||||
const std::string_view name) {
|
||||
const auto found = keyword.parameters.find(name);
|
||||
return found == keyword.parameters.end() ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
ParseDeckResult missing_parameter(
|
||||
const KeywordLine& keyword,
|
||||
const std::string_view parameter_name) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.missing_parameter",
|
||||
"Abaqus *" + keyword.keyword + " requires parameter " +
|
||||
std::string{parameter_name} + ".",
|
||||
keyword.source);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
std::ifstream input{path, std::ios::binary};
|
||||
if (!input) {
|
||||
return failure(
|
||||
DiagnosticStage::io,
|
||||
"abaqus.io.open_failed",
|
||||
"Unable to open Abaqus input file.",
|
||||
SourceLocation{path, 0U, 0U});
|
||||
}
|
||||
|
||||
ParsedDeck deck;
|
||||
Scope scope = Scope::global;
|
||||
std::optional<ParsedPart> current_part;
|
||||
std::optional<ParsedAssembly> current_assembly;
|
||||
std::optional<ParsedInstance> current_instance;
|
||||
DeckRecord* current_record = nullptr;
|
||||
|
||||
std::string line;
|
||||
std::size_t line_number = 0;
|
||||
while (std::getline(input, line)) {
|
||||
++line_number;
|
||||
if (line_number == 1U && line.starts_with("\xEF\xBB\xBF")) {
|
||||
line.erase(0, 3);
|
||||
}
|
||||
|
||||
const std::size_t first_nonspace =
|
||||
line.find_first_not_of(" \t\f\v\r");
|
||||
if (first_nonspace == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
const std::string_view content = trim(line);
|
||||
if (content.starts_with("**")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!content.starts_with('*')) {
|
||||
const std::vector<std::string> fields = split_fields(content);
|
||||
if (scope == Scope::instance) {
|
||||
current_instance->transform_data.push_back(fields);
|
||||
} else if (current_record != nullptr) {
|
||||
current_record->data.push_back(fields);
|
||||
} else {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.data_without_keyword",
|
||||
"Abaqus data line has no preceding keyword record.",
|
||||
SourceLocation{path, line_number, first_nonspace + 1U});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const SourceLocation source{
|
||||
path,
|
||||
line_number,
|
||||
first_nonspace + 1U,
|
||||
};
|
||||
ParseDeckResult keyword_error;
|
||||
std::optional<KeywordLine> parsed =
|
||||
parse_keyword_line(content, source, keyword_error);
|
||||
if (!parsed.has_value()) {
|
||||
return keyword_error;
|
||||
}
|
||||
KeywordLine& keyword = *parsed;
|
||||
current_record = nullptr;
|
||||
|
||||
if (keyword.keyword == "PART") {
|
||||
if (scope != Scope::global) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_part_scope",
|
||||
"*PART is only valid in global input scope.",
|
||||
source);
|
||||
}
|
||||
const std::string* name = parameter(keyword, "NAME");
|
||||
if (name == nullptr || name->empty()) {
|
||||
return missing_parameter(keyword, "NAME");
|
||||
}
|
||||
current_part = ParsedPart{*name, {}, source};
|
||||
scope = Scope::part;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "END PART") {
|
||||
if (scope != Scope::part || !current_part.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unexpected_end_part",
|
||||
"*END PART does not match an open *PART.",
|
||||
source);
|
||||
}
|
||||
deck.parts.push_back(std::move(*current_part));
|
||||
current_part.reset();
|
||||
scope = Scope::global;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "ASSEMBLY") {
|
||||
if (scope != Scope::global) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_assembly_scope",
|
||||
"*ASSEMBLY is only valid in global input scope.",
|
||||
source);
|
||||
}
|
||||
if (deck.assembly.has_value() ||
|
||||
current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.multiple_assemblies",
|
||||
"ParsedDeck can preserve only one *ASSEMBLY.",
|
||||
source);
|
||||
}
|
||||
const std::string* name = parameter(keyword, "NAME");
|
||||
if (name == nullptr || name->empty()) {
|
||||
return missing_parameter(keyword, "NAME");
|
||||
}
|
||||
current_assembly = ParsedAssembly{*name, {}, {}, source};
|
||||
scope = Scope::assembly;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "END ASSEMBLY") {
|
||||
if (scope != Scope::assembly ||
|
||||
!current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unexpected_end_assembly",
|
||||
"*END ASSEMBLY does not match an open *ASSEMBLY.",
|
||||
source);
|
||||
}
|
||||
deck.assembly = std::move(*current_assembly);
|
||||
current_assembly.reset();
|
||||
scope = Scope::global;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "INSTANCE") {
|
||||
if (scope != Scope::assembly ||
|
||||
!current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_instance_scope",
|
||||
"*INSTANCE is only valid in an open *ASSEMBLY.",
|
||||
source);
|
||||
}
|
||||
const std::string* name = parameter(keyword, "NAME");
|
||||
if (name == nullptr || name->empty()) {
|
||||
return missing_parameter(keyword, "NAME");
|
||||
}
|
||||
const std::string* part_name = parameter(keyword, "PART");
|
||||
if (part_name == nullptr || part_name->empty()) {
|
||||
return missing_parameter(keyword, "PART");
|
||||
}
|
||||
current_instance =
|
||||
ParsedInstance{*name, *part_name, {}, source};
|
||||
scope = Scope::instance;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "END INSTANCE") {
|
||||
if (scope != Scope::instance ||
|
||||
!current_instance.has_value() ||
|
||||
!current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unexpected_end_instance",
|
||||
"*END INSTANCE does not match an open *INSTANCE.",
|
||||
source);
|
||||
}
|
||||
current_assembly->instances.push_back(
|
||||
std::move(*current_instance));
|
||||
current_instance.reset();
|
||||
scope = Scope::assembly;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_supported_record(keyword.keyword)) {
|
||||
return syntax_failure(
|
||||
"abaqus.unsupported_keyword",
|
||||
"Unsupported Abaqus keyword *" + keyword.keyword + ".",
|
||||
source);
|
||||
}
|
||||
|
||||
DeckRecord next_record{
|
||||
std::move(keyword.keyword),
|
||||
std::move(keyword.parameters),
|
||||
{},
|
||||
source,
|
||||
};
|
||||
switch (scope) {
|
||||
case Scope::global:
|
||||
deck.global_records.push_back(std::move(next_record));
|
||||
current_record = &deck.global_records.back();
|
||||
break;
|
||||
case Scope::part:
|
||||
current_part->records.push_back(std::move(next_record));
|
||||
current_record = ¤t_part->records.back();
|
||||
break;
|
||||
case Scope::assembly:
|
||||
current_assembly->records.push_back(
|
||||
std::move(next_record));
|
||||
current_record = ¤t_assembly->records.back();
|
||||
break;
|
||||
case Scope::instance:
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.instance_local_keyword",
|
||||
"Keyword records inside *INSTANCE are unsupported.",
|
||||
source);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.bad()) {
|
||||
return failure(
|
||||
DiagnosticStage::io,
|
||||
"abaqus.io.read_failed",
|
||||
"Failed while reading Abaqus input file.",
|
||||
SourceLocation{path, line_number, 0U});
|
||||
}
|
||||
|
||||
if (current_instance.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unclosed_instance",
|
||||
"Abaqus *INSTANCE is not closed by *END INSTANCE.",
|
||||
current_instance->source);
|
||||
}
|
||||
if (current_part.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unclosed_part",
|
||||
"Abaqus *PART is not closed by *END PART.",
|
||||
current_part->source);
|
||||
}
|
||||
if (current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unclosed_assembly",
|
||||
"Abaqus *ASSEMBLY is not closed by *END ASSEMBLY.",
|
||||
current_assembly->source);
|
||||
}
|
||||
|
||||
return {std::move(deck), {}};
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
@@ -110,3 +110,33 @@ add_test(
|
||||
NAME DomainValidation
|
||||
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=*DomainValidation*
|
||||
)
|
||||
|
||||
add_executable(fesa_abaqus_parser_tests
|
||||
unit/io/abaqus/parser_test.cpp
|
||||
)
|
||||
|
||||
target_compile_features(fesa_abaqus_parser_tests PRIVATE cxx_std_20)
|
||||
target_compile_options(fesa_abaqus_parser_tests PRIVATE /W4 /permissive- /EHsc)
|
||||
target_compile_definitions(
|
||||
fesa_abaqus_parser_tests
|
||||
PRIVATE
|
||||
FESA_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
target_link_libraries(fesa_abaqus_parser_tests
|
||||
PRIVATE
|
||||
fesa_core
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME AbaqusParser
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=AbaqusParser.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ScopedDeck
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=ScopedDeck.*
|
||||
)
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
** Minimal flat/orphan-mesh cantilever syntax fixture.
|
||||
|
||||
*nOdE
|
||||
1, 0.0, 0.0, 0.0
|
||||
** Comments and blank lines do not terminate the current data record.
|
||||
|
||||
2, 1.0, 0.0, 0.0,
|
||||
*eLeMeNt, TYPE=B31, elset=Beam
|
||||
1, 1, 2
|
||||
*NSET, NSET=Fixed
|
||||
1
|
||||
*ELSET, ELSET=Beam
|
||||
1
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
210000.0, 0.3
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 1.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*BOUNDARY
|
||||
Fixed, 1, 6
|
||||
*STEP, NAME=Load
|
||||
*STATIC
|
||||
*CLOAD
|
||||
2, 2, -1.0
|
||||
*END STEP
|
||||
@@ -0,0 +1,37 @@
|
||||
** Minimal scoped cantilever syntax fixture.
|
||||
*PART, NAME=BeamPart
|
||||
*NODE
|
||||
1, 0.0, 0.0, 0.0
|
||||
2, 1.0, 0.0, 0.0
|
||||
*ELEMENT, TYPE=B31, ELSET=Beam
|
||||
1, 1, 2
|
||||
*NSET, NSET=Fixed
|
||||
1
|
||||
*NSET, NSET=Tip
|
||||
2
|
||||
*ELSET, ELSET=Beam
|
||||
1
|
||||
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||
1.0, 1.0, 1.0, 1.0, 1.0
|
||||
0.0, 1.0, 0.0
|
||||
*END PART
|
||||
|
||||
*ASSEMBLY, NAME=RootAssembly
|
||||
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||
*END INSTANCE
|
||||
*NSET, NSET=Fixed, INSTANCE=Beam-1
|
||||
1
|
||||
*NSET, NSET=Tip, INSTANCE=Beam-1
|
||||
2
|
||||
*END ASSEMBLY
|
||||
|
||||
*MATERIAL, NAME=Steel
|
||||
*ELASTIC
|
||||
210000.0, 0.3
|
||||
*STEP, NAME=Load
|
||||
*STATIC
|
||||
*BOUNDARY
|
||||
Fixed, 1, 6
|
||||
*CLOAD
|
||||
Tip, 2, -1.0
|
||||
*END STEP
|
||||
@@ -0,0 +1,235 @@
|
||||
#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(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
|
||||
Reference in New Issue
Block a user