feat(abaqus-subset-completion): step 2 — single-instance-semantic-validation
This commit is contained in:
@@ -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