feat(abaqus-subset-completion): step 1 — part-and-assembly-set-resolution
This commit is contained in:
@@ -120,6 +120,7 @@ add_test(
|
||||
add_executable(fesa_abaqus_parser_tests
|
||||
unit/io/abaqus/input_contract_test.cpp
|
||||
unit/io/abaqus/parser_test.cpp
|
||||
unit/io/abaqus/set_resolution_test.cpp
|
||||
)
|
||||
|
||||
target_compile_features(fesa_abaqus_parser_tests PRIVATE cxx_std_20)
|
||||
@@ -156,6 +157,24 @@ add_test(
|
||||
# Steps 1-4 make this normative matrix pass, then remove WILL_FAIL.
|
||||
set_tests_properties(AbaqusInputContract PROPERTIES WILL_FAIL TRUE)
|
||||
|
||||
add_test(
|
||||
NAME SetResolution
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=SetResolution.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME PartSet
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=PartSet.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME AssemblySet
|
||||
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||
--gtest_filter=AssemblySet.*
|
||||
)
|
||||
|
||||
add_executable(fesa_deck_to_domain_tests
|
||||
integration/io/minimal_deck_to_domain_test.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
#include <fesa/io/abaqus/parser.hpp>
|
||||
#include <fesa/io/abaqus/set_resolver.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <tuple>
|
||||
#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_;
|
||||
};
|
||||
|
||||
fesa::SetResolutionResult parse_and_resolve(
|
||||
const TemporaryDeck& input) {
|
||||
const fesa::ParseDeckResult parsed = fesa::parse_deck(input.path());
|
||||
if (!parsed.deck.has_value()) {
|
||||
throw std::runtime_error{"Test deck did not parse."};
|
||||
}
|
||||
return fesa::resolve_sets(*parsed.deck);
|
||||
}
|
||||
|
||||
const fesa::ResolvedSet& find_set(
|
||||
const fesa::SetResolutionResult& result,
|
||||
const fesa::ResolvedSetScope scope,
|
||||
const std::string_view scope_name,
|
||||
const fesa::ResolvedSetKind kind,
|
||||
const std::string_view set_name) {
|
||||
const auto found = std::ranges::find_if(
|
||||
result.sets,
|
||||
[=](const fesa::ResolvedSet& set) {
|
||||
return set.scope == scope && set.scope_name == scope_name &&
|
||||
set.kind == kind && set.set_name == set_name;
|
||||
});
|
||||
if (found == result.sets.end()) {
|
||||
throw std::runtime_error{"Expected resolved set was not found."};
|
||||
}
|
||||
return *found;
|
||||
}
|
||||
|
||||
const fesa::Diagnostic& find_diagnostic(
|
||||
const fesa::SetResolutionResult& result,
|
||||
const std::string_view code,
|
||||
const std::size_t line) {
|
||||
const auto found = std::ranges::find_if(
|
||||
result.diagnostics,
|
||||
[=](const fesa::Diagnostic& diagnostic) {
|
||||
return diagnostic.code == code && diagnostic.source.has_value() &&
|
||||
diagnostic.source->line == line;
|
||||
});
|
||||
if (found == result.diagnostics.end()) {
|
||||
throw std::runtime_error{"Expected set diagnostic was not found."};
|
||||
}
|
||||
return *found;
|
||||
}
|
||||
|
||||
TEST(
|
||||
SetResolution,
|
||||
CanonicalizesExplicitGenerateNestedForwardDuplicateAndEmptySets) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-valid.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"2, 1, 0, 0\n"
|
||||
"3, 2, 0, 0\n"
|
||||
"*ELEMENT, TYPE=B31, ELSET=ImplicitElements\n"
|
||||
"10, 1, 2\n"
|
||||
"20, 2, 3\n"
|
||||
"*NSET, NSET=AllNodes\n"
|
||||
"GeneratedNodes, 3, 1, 3\n"
|
||||
"*NSET, NSET=GeneratedNodes, GENERATE\n"
|
||||
"1, 3, 1\n"
|
||||
"*NSET, NSET=EmptyNodes\n"
|
||||
"*ELSET, ELSET=AllElements\n"
|
||||
"GeneratedElements, ImplicitElements, 20\n"
|
||||
"*ELSET, ELSET=GeneratedElements, GENERATE\n"
|
||||
"10, 20, 10\n"
|
||||
"*END PART\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"AllNodes")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1, 2, 3}));
|
||||
EXPECT_TRUE(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"EmptyNodes")
|
||||
.sorted_unique_labels.empty());
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::element,
|
||||
"AllElements")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{10, 20}));
|
||||
}
|
||||
|
||||
TEST(PartSet, KeepsNodeAndElementSetNamespacesSeparate) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-part-set-kind-collision.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"2, 1, 0, 0\n"
|
||||
"*ELEMENT, TYPE=B31\n"
|
||||
"1, 1, 2\n"
|
||||
"*NSET, NSET=Shared\n"
|
||||
"1\n"
|
||||
"*ELSET, ELSET=Shared\n"
|
||||
"1\n"
|
||||
"*END PART\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1}));
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::element,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1}));
|
||||
}
|
||||
|
||||
TEST(AssemblySet, KeepsPartScopeSeparateAndLiftsActivePartLabels) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-assembly-set-scope-collision.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"2, 1, 0, 0\n"
|
||||
"*NSET, NSET=Shared\n"
|
||||
"1\n"
|
||||
"*END PART\n"
|
||||
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
|
||||
"*END INSTANCE\n"
|
||||
"*NSET, NSET=Shared, INSTANCE=Beam-1\n"
|
||||
"2, 2\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
ASSERT_TRUE(result.diagnostics.empty());
|
||||
ASSERT_EQ(result.sets.size(), 2U);
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::part,
|
||||
"BeamPart",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{1}));
|
||||
EXPECT_EQ(
|
||||
find_set(
|
||||
result,
|
||||
fesa::ResolvedSetScope::assembly,
|
||||
"RootAssembly",
|
||||
fesa::ResolvedSetKind::node,
|
||||
"Shared")
|
||||
.sorted_unique_labels,
|
||||
(std::vector<std::int64_t>{2}));
|
||||
EXPECT_TRUE(std::ranges::is_sorted(
|
||||
result.sets,
|
||||
[](const fesa::ResolvedSet& left, const fesa::ResolvedSet& right) {
|
||||
return std::tuple{
|
||||
left.scope,
|
||||
left.scope_name,
|
||||
left.kind,
|
||||
left.set_name} <
|
||||
std::tuple{
|
||||
right.scope,
|
||||
right.scope_name,
|
||||
right.kind,
|
||||
right.set_name};
|
||||
}));
|
||||
}
|
||||
|
||||
TEST(SetResolution, ReportsCycleAtTheClosingReferenceSource) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-cycle.inp",
|
||||
"*NSET, NSET=First\n"
|
||||
"Second\n"
|
||||
"*NSET, NSET=Second\n"
|
||||
"First\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
const fesa::Diagnostic& diagnostic =
|
||||
find_diagnostic(result, "abaqus.semantic.set_cycle", 4U);
|
||||
EXPECT_EQ(diagnostic.stage, fesa::DiagnosticStage::semantic);
|
||||
EXPECT_EQ(diagnostic.source->file, input.path());
|
||||
}
|
||||
|
||||
TEST(SetResolution, ReportsUnknownSetAndEntityAtTheirMemberRows) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-missing.inp",
|
||||
"*NODE\n"
|
||||
"1, 0, 0, 0\n"
|
||||
"*NSET, NSET=MissingEntity\n"
|
||||
"2\n"
|
||||
"*NSET, NSET=MissingSet\n"
|
||||
"Unknown\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
EXPECT_EQ(result.diagnostics.size(), 2U);
|
||||
EXPECT_EQ(
|
||||
find_diagnostic(
|
||||
result, "abaqus.semantic.missing_set_member", 4U)
|
||||
.stage,
|
||||
fesa::DiagnosticStage::semantic);
|
||||
EXPECT_EQ(
|
||||
find_diagnostic(
|
||||
result, "abaqus.semantic.missing_set_member", 6U)
|
||||
.stage,
|
||||
fesa::DiagnosticStage::semantic);
|
||||
}
|
||||
|
||||
TEST(SetResolution, ReportsInvalidGenerateRangesInInputOrder) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-set-resolution-generate-invalid.inp",
|
||||
"*NSET, NSET=Reversed, GENERATE\n"
|
||||
"3, 1, 1\n"
|
||||
"*NSET, NSET=NotDivisible, GENERATE\n"
|
||||
"1, 4, 2\n"
|
||||
"*ELSET, ELSET=WrongFieldCount, GENERATE\n"
|
||||
"1, 2\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
ASSERT_EQ(result.diagnostics.size(), 3U);
|
||||
for (std::size_t index = 0; index < result.diagnostics.size(); ++index) {
|
||||
EXPECT_EQ(
|
||||
result.diagnostics[index].code,
|
||||
"abaqus.semantic.invalid_generate");
|
||||
ASSERT_TRUE(result.diagnostics[index].source.has_value());
|
||||
}
|
||||
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
|
||||
EXPECT_EQ(result.diagnostics[1].source->line, 4U);
|
||||
EXPECT_EQ(result.diagnostics[2].source->line, 6U);
|
||||
}
|
||||
|
||||
TEST(AssemblySet, RejectsAnInstanceOtherThanTheSingleActiveInstance) {
|
||||
const TemporaryDeck input{
|
||||
"fesa-assembly-set-wrong-instance.inp",
|
||||
"*PART, NAME=BeamPart\n"
|
||||
"*NODE\n"
|
||||
"1, 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=Other\n"
|
||||
"1\n"
|
||||
"*END ASSEMBLY\n"};
|
||||
|
||||
const fesa::SetResolutionResult result = parse_and_resolve(input);
|
||||
|
||||
EXPECT_TRUE(result.sets.empty());
|
||||
EXPECT_EQ(
|
||||
find_diagnostic(result, "abaqus.semantic.wrong_instance", 8U)
|
||||
.stage,
|
||||
fesa::DiagnosticStage::semantic);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user