feat(abaqus-subset-completion): step 2 — single-instance-semantic-validation
This commit is contained in:
@@ -36,6 +36,7 @@ add_library(fesa_core STATIC
|
|||||||
src/fesa/fem/dof_manager.cpp
|
src/fesa/fem/dof_manager.cpp
|
||||||
src/fesa/fem/gauss_rule.cpp
|
src/fesa/fem/gauss_rule.cpp
|
||||||
src/fesa/fem/line2_shape.cpp
|
src/fesa/fem/line2_shape.cpp
|
||||||
|
src/fesa/io/abaqus/active_input.cpp
|
||||||
src/fesa/io/abaqus/parser.cpp
|
src/fesa/io/abaqus/parser.cpp
|
||||||
src/fesa/io/abaqus/semantic_mapper.cpp
|
src/fesa/io/abaqus/semantic_mapper.cpp
|
||||||
src/fesa/io/abaqus/set_resolver.cpp
|
src/fesa/io/abaqus/set_resolver.cpp
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
#include <fesa/io/abaqus/deck_record.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct ActiveInputView final {
|
||||||
|
bool flat = true;
|
||||||
|
std::string part_name;
|
||||||
|
std::string instance_name;
|
||||||
|
std::span<const DeckRecord> part_records;
|
||||||
|
std::span<const DeckRecord> assembly_records;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ActiveInputResult final {
|
||||||
|
std::optional<ActiveInputView> input;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] ActiveInputResult select_active_input(const ParsedDeck& deck);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -28,6 +28,7 @@ struct ParsedInstance final {
|
|||||||
std::string name;
|
std::string name;
|
||||||
std::string part_name;
|
std::string part_name;
|
||||||
std::vector<std::vector<std::string>> transform_data;
|
std::vector<std::vector<std::string>> transform_data;
|
||||||
|
std::vector<SourceLocation> transform_sources;
|
||||||
SourceLocation source;
|
SourceLocation source;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
#include <fesa/io/abaqus/active_input.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const std::string* parameter(
|
||||||
|
const DeckRecord& record,
|
||||||
|
const std::string_view name) {
|
||||||
|
const auto found = record.parameters.find(name);
|
||||||
|
return found == record.parameters.end() ? nullptr : &found->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_mesh_record(const DeckRecord& record) {
|
||||||
|
return record.keyword == "NODE" || record.keyword == "ELEMENT";
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveInputResult failure(
|
||||||
|
std::string code,
|
||||||
|
std::string message,
|
||||||
|
const SourceLocation& source) {
|
||||||
|
return {
|
||||||
|
std::nullopt,
|
||||||
|
{{
|
||||||
|
DiagnosticStage::semantic,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
source,
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ActiveInputResult select_active_input(const ParsedDeck& deck) {
|
||||||
|
const bool hierarchical =
|
||||||
|
!deck.parts.empty() || deck.assembly.has_value();
|
||||||
|
if (!hierarchical) {
|
||||||
|
return {
|
||||||
|
ActiveInputView{
|
||||||
|
true,
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
deck.global_records,
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto flat_mesh =
|
||||||
|
std::ranges::find_if(deck.global_records, is_mesh_record);
|
||||||
|
if (flat_mesh != deck.global_records.end()) {
|
||||||
|
return failure(
|
||||||
|
"abaqus.semantic.mixed_mesh_organization",
|
||||||
|
"Flat mesh records cannot be mixed with Part/Assembly input.",
|
||||||
|
flat_mesh->source);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deck.assembly.has_value()) {
|
||||||
|
const SourceLocation source =
|
||||||
|
deck.parts.empty() ? SourceLocation{} : deck.parts.front().source;
|
||||||
|
return failure(
|
||||||
|
"abaqus.semantic.assembly_count",
|
||||||
|
"Hierarchical Phase 1 input requires exactly one Assembly.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ParsedAssembly& assembly = *deck.assembly;
|
||||||
|
if (assembly.instances.size() != 1U) {
|
||||||
|
const SourceLocation& source = assembly.instances.size() > 1U
|
||||||
|
? assembly.instances[1].source
|
||||||
|
: assembly.source;
|
||||||
|
return failure(
|
||||||
|
"abaqus.semantic.instance_count",
|
||||||
|
"Phase 1 requires exactly one Instance.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ParsedInstance& instance = assembly.instances.front();
|
||||||
|
if (!instance.transform_data.empty()) {
|
||||||
|
const SourceLocation& source = instance.transform_sources.empty()
|
||||||
|
? instance.source
|
||||||
|
: instance.transform_sources.front();
|
||||||
|
return failure(
|
||||||
|
"abaqus.semantic.instance_transform",
|
||||||
|
"Instance translation and rotation data are unsupported.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto part =
|
||||||
|
std::ranges::find(deck.parts, instance.part_name, &ParsedPart::name);
|
||||||
|
if (part == deck.parts.end()) {
|
||||||
|
return failure(
|
||||||
|
"abaqus.semantic.missing_part",
|
||||||
|
"Instance '" + instance.name + "' references missing Part '" +
|
||||||
|
instance.part_name + "'.",
|
||||||
|
instance.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const DeckRecord& record : assembly.records) {
|
||||||
|
if (record.keyword != "NSET" && record.keyword != "ELSET") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string* record_instance = parameter(record, "INSTANCE");
|
||||||
|
if (record_instance == nullptr || *record_instance != instance.name) {
|
||||||
|
const std::string* name = parameter(
|
||||||
|
record, record.keyword == "NSET" ? "NSET" : "ELSET");
|
||||||
|
return failure(
|
||||||
|
"abaqus.semantic.wrong_instance",
|
||||||
|
"Assembly set '" +
|
||||||
|
(name == nullptr ? std::string{} : *name) +
|
||||||
|
"' must reference the active Instance.",
|
||||||
|
record.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ActiveInputView{
|
||||||
|
false,
|
||||||
|
part->name,
|
||||||
|
instance.name,
|
||||||
|
part->records,
|
||||||
|
assembly.records,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -214,6 +214,11 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
|||||||
const std::vector<std::string> fields = split_fields(content);
|
const std::vector<std::string> fields = split_fields(content);
|
||||||
if (scope == Scope::instance) {
|
if (scope == Scope::instance) {
|
||||||
current_instance->transform_data.push_back(fields);
|
current_instance->transform_data.push_back(fields);
|
||||||
|
current_instance->transform_sources.push_back(SourceLocation{
|
||||||
|
path,
|
||||||
|
line_number,
|
||||||
|
first_nonspace + 1U,
|
||||||
|
});
|
||||||
} else if (current_record != nullptr) {
|
} else if (current_record != nullptr) {
|
||||||
current_record->data.push_back(fields);
|
current_record->data.push_back(fields);
|
||||||
current_record->data_sources.push_back(SourceLocation{
|
current_record->data_sources.push_back(SourceLocation{
|
||||||
@@ -327,7 +332,7 @@ ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
|||||||
return missing_parameter(keyword, "PART");
|
return missing_parameter(keyword, "PART");
|
||||||
}
|
}
|
||||||
current_instance =
|
current_instance =
|
||||||
ParsedInstance{*name, *part_name, {}, source};
|
ParsedInstance{*name, *part_name, {}, {}, source};
|
||||||
scope = Scope::instance;
|
scope = Scope::instance;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include <fesa/io/abaqus/semantic_mapper.hpp>
|
#include <fesa/io/abaqus/semantic_mapper.hpp>
|
||||||
|
|
||||||
|
#include <fesa/io/abaqus/active_input.hpp>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <charconv>
|
#include <charconv>
|
||||||
@@ -26,24 +28,26 @@ const std::string* parameter(
|
|||||||
return found == record.parameters.end() ? nullptr : &found->second;
|
return found == record.parameters.end() ? nullptr : &found->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_mesh_record(const DeckRecord& record) {
|
|
||||||
return record.keyword == "NODE" || record.keyword == "ELEMENT";
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeckMapper final {
|
class DeckMapper final {
|
||||||
public:
|
public:
|
||||||
explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {}
|
explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {}
|
||||||
|
|
||||||
[[nodiscard]] DomainBuildResult map() {
|
[[nodiscard]] DomainBuildResult map() {
|
||||||
if (!select_active_scope()) {
|
ActiveInputResult selected = select_active_input(deck_);
|
||||||
|
if (!selected.input.has_value()) {
|
||||||
|
diagnostics_ = std::move(selected.diagnostics);
|
||||||
return failure();
|
return failure();
|
||||||
}
|
}
|
||||||
|
active_records_ = selected.input->part_records;
|
||||||
|
assembly_records_ = selected.input->assembly_records;
|
||||||
|
part_name_ = std::move(selected.input->part_name);
|
||||||
|
instance_name_ = std::move(selected.input->instance_name);
|
||||||
|
|
||||||
collect_materials();
|
collect_materials();
|
||||||
collect_nodes();
|
collect_nodes();
|
||||||
collect_raw_sets(*active_records_, false);
|
collect_raw_sets(active_records_, false);
|
||||||
if (active_assembly_ != nullptr) {
|
if (!assembly_records_.empty()) {
|
||||||
collect_raw_sets(active_assembly_->records, true);
|
collect_raw_sets(assembly_records_, true);
|
||||||
}
|
}
|
||||||
collect_sections();
|
collect_sections();
|
||||||
collect_elements();
|
collect_elements();
|
||||||
@@ -153,71 +157,6 @@ private:
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool select_active_scope() {
|
|
||||||
const bool has_hierarchical_input =
|
|
||||||
!deck_.parts.empty() || deck_.assembly.has_value();
|
|
||||||
if (!has_hierarchical_input) {
|
|
||||||
active_records_ = &deck_.global_records;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std::ranges::any_of(deck_.global_records, is_mesh_record)) {
|
|
||||||
add_error(
|
|
||||||
"abaqus.semantic.mixed_mesh_organization",
|
|
||||||
"Flat mesh records cannot be mixed with Part/Assembly input.",
|
|
||||||
std::ranges::find_if(
|
|
||||||
deck_.global_records, is_mesh_record)->source);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!deck_.assembly.has_value()) {
|
|
||||||
const SourceLocation source =
|
|
||||||
deck_.parts.empty() ? SourceLocation{} : deck_.parts[0].source;
|
|
||||||
add_error(
|
|
||||||
"abaqus.semantic.assembly_count",
|
|
||||||
"Hierarchical Phase 1 input requires exactly one Assembly.",
|
|
||||||
source);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
active_assembly_ = &*deck_.assembly;
|
|
||||||
if (active_assembly_->instances.size() != 1U) {
|
|
||||||
const SourceLocation& source =
|
|
||||||
active_assembly_->instances.size() > 1U
|
|
||||||
? active_assembly_->instances[1].source
|
|
||||||
: active_assembly_->source;
|
|
||||||
add_error(
|
|
||||||
"abaqus.semantic.instance_count",
|
|
||||||
"Phase 1 requires exactly one Instance.",
|
|
||||||
source);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ParsedInstance& instance = active_assembly_->instances.front();
|
|
||||||
if (!instance.transform_data.empty()) {
|
|
||||||
add_error(
|
|
||||||
"abaqus.semantic.instance_transform",
|
|
||||||
"Instance translation and rotation data are unsupported.",
|
|
||||||
instance.source);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto part = std::ranges::find(
|
|
||||||
deck_.parts, instance.part_name, &ParsedPart::name);
|
|
||||||
if (part == deck_.parts.end()) {
|
|
||||||
add_error(
|
|
||||||
"abaqus.semantic.missing_part",
|
|
||||||
"Instance '" + instance.name + "' references missing Part '" +
|
|
||||||
instance.part_name + "'.",
|
|
||||||
instance.source);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
active_records_ = &part->records;
|
|
||||||
part_name_ = part->name;
|
|
||||||
instance_name_ = instance.name;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void collect_materials() {
|
void collect_materials() {
|
||||||
const std::string* current_name = nullptr;
|
const std::string* current_name = nullptr;
|
||||||
for (const DeckRecord& record : deck_.global_records) {
|
for (const DeckRecord& record : deck_.global_records) {
|
||||||
@@ -264,7 +203,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void collect_nodes() {
|
void collect_nodes() {
|
||||||
for (const DeckRecord& record : *active_records_) {
|
for (const DeckRecord& record : active_records_) {
|
||||||
if (record.keyword != "NODE") {
|
if (record.keyword != "NODE") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -298,7 +237,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void collect_raw_sets(
|
void collect_raw_sets(
|
||||||
const std::vector<DeckRecord>& records,
|
const std::span<const DeckRecord> records,
|
||||||
const bool assembly_scope) {
|
const bool assembly_scope) {
|
||||||
for (const DeckRecord& record : records) {
|
for (const DeckRecord& record : records) {
|
||||||
const bool is_node_set = record.keyword == "NSET";
|
const bool is_node_set = record.keyword == "NSET";
|
||||||
@@ -343,7 +282,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void collect_sections() {
|
void collect_sections() {
|
||||||
for (const DeckRecord& record : *active_records_) {
|
for (const DeckRecord& record : active_records_) {
|
||||||
if (record.keyword != "BEAM GENERAL SECTION") {
|
if (record.keyword != "BEAM GENERAL SECTION") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -422,7 +361,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void collect_elements() {
|
void collect_elements() {
|
||||||
for (const DeckRecord& record : *active_records_) {
|
for (const DeckRecord& record : active_records_) {
|
||||||
if (record.keyword != "ELEMENT") {
|
if (record.keyword != "ELEMENT") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -661,8 +600,8 @@ private:
|
|||||||
const ParsedDeck& deck_;
|
const ParsedDeck& deck_;
|
||||||
DomainBuilder builder_;
|
DomainBuilder builder_;
|
||||||
std::vector<Diagnostic> diagnostics_;
|
std::vector<Diagnostic> diagnostics_;
|
||||||
const std::vector<DeckRecord>* active_records_ = nullptr;
|
std::span<const DeckRecord> active_records_;
|
||||||
const ParsedAssembly* active_assembly_ = nullptr;
|
std::span<const DeckRecord> assembly_records_;
|
||||||
std::string part_name_;
|
std::string part_name_;
|
||||||
std::string instance_name_;
|
std::string instance_name_;
|
||||||
LabelMap node_ids_;
|
LabelMap node_ids_;
|
||||||
|
|||||||
+14
-1
@@ -118,6 +118,7 @@ add_test(
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_executable(fesa_abaqus_parser_tests
|
add_executable(fesa_abaqus_parser_tests
|
||||||
|
unit/io/abaqus/active_input_test.cpp
|
||||||
unit/io/abaqus/input_contract_test.cpp
|
unit/io/abaqus/input_contract_test.cpp
|
||||||
unit/io/abaqus/parser_test.cpp
|
unit/io/abaqus/parser_test.cpp
|
||||||
unit/io/abaqus/set_resolution_test.cpp
|
unit/io/abaqus/set_resolution_test.cpp
|
||||||
@@ -175,6 +176,18 @@ add_test(
|
|||||||
--gtest_filter=AssemblySet.*
|
--gtest_filter=AssemblySet.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ActiveInput
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||||
|
--gtest_filter=ActiveInput.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME SemanticScope
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||||
|
--gtest_filter=SemanticScope.*
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(fesa_deck_to_domain_tests
|
add_executable(fesa_deck_to_domain_tests
|
||||||
integration/io/minimal_deck_to_domain_test.cpp
|
integration/io/minimal_deck_to_domain_test.cpp
|
||||||
)
|
)
|
||||||
@@ -200,7 +213,7 @@ add_test(
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_test(
|
add_test(
|
||||||
NAME ActiveInstance
|
NAME SingleInstance
|
||||||
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
|
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
|
||||||
--gtest_filter=ActiveInstance.*
|
--gtest_filter=ActiveInstance.*
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ TEST(ActiveInstance, RejectsInstanceTransformWithSourceDiagnostic) {
|
|||||||
ASSERT_TRUE(has_diagnostic(result, "abaqus.semantic.instance_transform"));
|
ASSERT_TRUE(has_diagnostic(result, "abaqus.semantic.instance_transform"));
|
||||||
ASSERT_FALSE(result.diagnostics.empty());
|
ASSERT_FALSE(result.diagnostics.empty());
|
||||||
ASSERT_TRUE(result.diagnostics.front().source.has_value());
|
ASSERT_TRUE(result.diagnostics.front().source.has_value());
|
||||||
EXPECT_EQ(result.diagnostics.front().source->line, 4U);
|
EXPECT_EQ(result.diagnostics.front().source->line, 5U);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ActiveInstance, RejectsMissingPartReferenceWithSourceDiagnostic) {
|
TEST(ActiveInstance, RejectsMissingPartReferenceWithSourceDiagnostic) {
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#include <fesa/io/abaqus/active_input.hpp>
|
||||||
|
#include <fesa/io/abaqus/parser.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::ParsedDeck parse(const std::filesystem::path& path) {
|
||||||
|
auto parsed = fesa::parse_deck(path);
|
||||||
|
if (!parsed.deck.has_value()) {
|
||||||
|
throw std::runtime_error{"Test deck did not parse."};
|
||||||
|
}
|
||||||
|
return std::move(*parsed.deck);
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Diagnostic expect_failure(
|
||||||
|
const fesa::ParsedDeck& deck,
|
||||||
|
const std::string_view code,
|
||||||
|
const std::size_t line) {
|
||||||
|
const fesa::ActiveInputResult result = fesa::select_active_input(deck);
|
||||||
|
EXPECT_FALSE(result.input.has_value());
|
||||||
|
const auto diagnostic = std::ranges::find(
|
||||||
|
result.diagnostics, code, &fesa::Diagnostic::code);
|
||||||
|
if (diagnostic == result.diagnostics.end()) {
|
||||||
|
throw std::runtime_error{"Expected active-input diagnostic was not found."};
|
||||||
|
}
|
||||||
|
EXPECT_EQ(diagnostic->stage, fesa::DiagnosticStage::semantic);
|
||||||
|
EXPECT_TRUE(diagnostic->source.has_value());
|
||||||
|
if (diagnostic->source.has_value()) {
|
||||||
|
EXPECT_EQ(diagnostic->source->line, line);
|
||||||
|
}
|
||||||
|
return *diagnostic;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ActiveInput, SelectsFlatGlobalRecords) {
|
||||||
|
const fesa::ParsedDeck deck =
|
||||||
|
parse(fixture_path("minimal_cantilever.inp"));
|
||||||
|
|
||||||
|
const fesa::ActiveInputResult result = fesa::select_active_input(deck);
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.input.has_value());
|
||||||
|
EXPECT_TRUE(result.diagnostics.empty());
|
||||||
|
EXPECT_TRUE(result.input->flat);
|
||||||
|
EXPECT_TRUE(result.input->part_name.empty());
|
||||||
|
EXPECT_TRUE(result.input->instance_name.empty());
|
||||||
|
EXPECT_EQ(result.input->part_records.data(), deck.global_records.data());
|
||||||
|
EXPECT_EQ(result.input->part_records.size(), deck.global_records.size());
|
||||||
|
EXPECT_TRUE(result.input->assembly_records.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ActiveInput, SelectsOnlyTheReferencedPartAndAssemblyRecords) {
|
||||||
|
const TemporaryDeck input{
|
||||||
|
"fesa-active-input-selection.inp",
|
||||||
|
"*PART, NAME=Unused\n"
|
||||||
|
"*NODE\n"
|
||||||
|
"99, 9.0, 0.0, 0.0\n"
|
||||||
|
"*END PART\n"
|
||||||
|
"*PART, NAME=BeamPart\n"
|
||||||
|
"*NODE\n"
|
||||||
|
"1, 0.0, 0.0, 0.0\n"
|
||||||
|
"*END PART\n"
|
||||||
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||||
|
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||||
|
"*END INSTANCE\n"
|
||||||
|
"*NSET, NSET=Fixed, INSTANCE=Beam-1\n"
|
||||||
|
"1\n"
|
||||||
|
"*END ASSEMBLY\n"};
|
||||||
|
const fesa::ParsedDeck deck = parse(input.path());
|
||||||
|
|
||||||
|
const fesa::ActiveInputResult result = fesa::select_active_input(deck);
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.input.has_value());
|
||||||
|
EXPECT_FALSE(result.input->flat);
|
||||||
|
EXPECT_EQ(result.input->part_name, "BeamPart");
|
||||||
|
EXPECT_EQ(result.input->instance_name, "Beam-1");
|
||||||
|
ASSERT_EQ(result.input->part_records.size(), 1U);
|
||||||
|
EXPECT_EQ(result.input->part_records[0].keyword, "NODE");
|
||||||
|
ASSERT_EQ(result.input->assembly_records.size(), 1U);
|
||||||
|
EXPECT_EQ(result.input->assembly_records[0].keyword, "NSET");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ActiveInput, RejectsMixedFlatAndHierarchicalMeshAtTheFlatRecord) {
|
||||||
|
const fesa::ParsedDeck deck =
|
||||||
|
parse(fixture_path("invalid/mixed_mesh.inp"));
|
||||||
|
|
||||||
|
const fesa::Diagnostic diagnostic = expect_failure(
|
||||||
|
deck, "abaqus.semantic.mixed_mesh_organization", 1U);
|
||||||
|
|
||||||
|
EXPECT_EQ(diagnostic.source->file, fixture_path("invalid/mixed_mesh.inp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ActiveInput, RejectsHierarchicalInputWithoutAnAssembly) {
|
||||||
|
const TemporaryDeck input{
|
||||||
|
"fesa-missing-assembly.inp",
|
||||||
|
"*PART, NAME=BeamPart\n"
|
||||||
|
"*END PART\n"};
|
||||||
|
const fesa::ParsedDeck deck = parse(input.path());
|
||||||
|
|
||||||
|
expect_failure(deck, "abaqus.semantic.assembly_count", 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ActiveInput, RejectsAnAssemblyWithoutExactlyOneInstance) {
|
||||||
|
const TemporaryDeck no_instance{
|
||||||
|
"fesa-no-instance.inp",
|
||||||
|
"*PART, NAME=BeamPart\n"
|
||||||
|
"*END PART\n"
|
||||||
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||||
|
"*END ASSEMBLY\n"};
|
||||||
|
const TemporaryDeck multiple_instances{
|
||||||
|
"fesa-multiple-instances.inp",
|
||||||
|
"*PART, NAME=BeamPart\n"
|
||||||
|
"*END PART\n"
|
||||||
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||||
|
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||||
|
"*END INSTANCE\n"
|
||||||
|
"*INSTANCE, NAME=Beam-2, PART=BeamPart\n"
|
||||||
|
"*END INSTANCE\n"
|
||||||
|
"*END ASSEMBLY\n"};
|
||||||
|
|
||||||
|
expect_failure(
|
||||||
|
parse(no_instance.path()), "abaqus.semantic.instance_count", 3U);
|
||||||
|
expect_failure(
|
||||||
|
parse(multiple_instances.path()),
|
||||||
|
"abaqus.semantic.instance_count",
|
||||||
|
6U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ActiveInput, RejectsTransformAtTheFirstTransformDataRow) {
|
||||||
|
const fesa::ParsedDeck deck =
|
||||||
|
parse(fixture_path("invalid/instance_transform.inp"));
|
||||||
|
|
||||||
|
expect_failure(deck, "abaqus.semantic.instance_transform", 5U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ActiveInput, RejectsAnInstanceThatReferencesAMissingPart) {
|
||||||
|
const TemporaryDeck input{
|
||||||
|
"fesa-missing-part.inp",
|
||||||
|
"*PART, NAME=OtherPart\n"
|
||||||
|
"*END PART\n"
|
||||||
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||||
|
"*INSTANCE, NAME=Beam-1, PART=MissingPart\n"
|
||||||
|
"*END INSTANCE\n"
|
||||||
|
"*END ASSEMBLY\n"};
|
||||||
|
const fesa::ParsedDeck deck = parse(input.path());
|
||||||
|
|
||||||
|
expect_failure(deck, "abaqus.semantic.missing_part", 4U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ActiveInput, RejectsAssemblySetsForAnotherInstance) {
|
||||||
|
const fesa::ParsedDeck deck =
|
||||||
|
parse(fixture_path("invalid/assembly_set_wrong_instance.inp"));
|
||||||
|
|
||||||
|
expect_failure(deck, "abaqus.semantic.wrong_instance", 6U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SemanticScope, RejectsInstanceLocalMeshAtTheKeywordRow) {
|
||||||
|
const auto path = fixture_path("invalid/instance_local_mesh.inp");
|
||||||
|
|
||||||
|
const fesa::ParseDeckResult result = fesa::parse_deck(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.syntax.instance_local_keyword");
|
||||||
|
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
||||||
|
EXPECT_EQ(result.diagnostics[0].source->file, path);
|
||||||
|
EXPECT_EQ(result.diagnostics[0].source->line, 5U);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user