feat(linear-static-3d-euler-beam): step 11 - inp-syntax-parser

This commit is contained in:
KOKO\Mimi
2026-08-09 15:19:29 +09:00
parent 3b56fc906b
commit b73f6cd823
8 changed files with 550 additions and 0 deletions
+2
View File
@@ -8,6 +8,8 @@ add_executable(
unit/core/status_test.cpp
unit/math/matrix_test.cpp
unit/math/vector_test.cpp
unit/io/abaqus/input_reader_test.cpp
unit/io/abaqus/input_syntax_test.cpp
unit/model/domain_test.cpp
unit/model/model_types_test.cpp
)
+133
View File
@@ -0,0 +1,133 @@
#include "fesa/io/abaqus/input_reader.hpp"
#include <gtest/gtest.h>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
namespace {
class TemporaryInputFile {
public:
TemporaryInputFile(const std::string& stem, const std::string& content)
: path_{std::filesystem::temp_directory_path() /
("fesa-" + stem + ".inp")} {
std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
if (!stream) {
throw std::runtime_error{"Unable to create INP reader test fixture."};
}
}
~TemporaryInputFile() {
std::error_code error;
std::filesystem::remove(path_, error);
}
const std::filesystem::path& path() const noexcept {
return path_;
}
private:
std::filesystem::path path_;
};
std::string readExactBytes(const std::filesystem::path& path) {
std::ifstream stream{path, std::ios::binary};
if (!stream) {
throw std::runtime_error{"Unable to read legacy INP fixture."};
}
return std::string{
std::istreambuf_iterator<char>{stream},
std::istreambuf_iterator<char>{}};
}
std::filesystem::path repositoryRoot() {
auto path = std::filesystem::path{__FILE__}.parent_path();
for (int parent = 0; parent < 4; ++parent) {
path = path.parent_path();
}
return path;
}
} // namespace
TEST(InpSyntax, RejectsMalformedOrOrphanData) {
const auto missingPath =
std::filesystem::temp_directory_path() / "fesa-missing-input.inp";
std::error_code removeError;
std::filesystem::remove(missingPath, removeError);
const auto unreadable = fesa::AbaqusInputReader{}.read(missingPath);
ASSERT_FALSE(unreadable.hasValue());
EXPECT_EQ(
unreadable.status().failureCategory(),
fesa::FailureCategory::input);
ASSERT_EQ(unreadable.status().diagnostics().size(), 1U);
EXPECT_EQ(unreadable.status().diagnostics()[0].code,
"input-file-unreadable");
const TemporaryInputFile malformed{"malformed-keyword", "*, name=value\n"};
const auto malformedResult =
fesa::AbaqusInputReader{}.read(malformed.path());
ASSERT_FALSE(malformedResult.hasValue());
EXPECT_EQ(
malformedResult.status().failureCategory(),
fesa::FailureCategory::input);
ASSERT_EQ(malformedResult.status().diagnostics().size(), 1U);
EXPECT_EQ(malformedResult.status().diagnostics()[0].code,
"malformed-keyword");
EXPECT_EQ(malformedResult.status().diagnostics()[0].location.line, 1U);
const TemporaryInputFile orphan{
"orphan-data", "** comment\n\norphan, data\n"};
const auto orphanResult = fesa::AbaqusInputReader{}.read(orphan.path());
ASSERT_FALSE(orphanResult.hasValue());
EXPECT_EQ(
orphanResult.status().failureCategory(),
fesa::FailureCategory::input);
ASSERT_EQ(orphanResult.status().diagnostics().size(), 1U);
EXPECT_EQ(orphanResult.status().diagnostics()[0].code,
"orphan-data-line");
EXPECT_EQ(orphanResult.status().diagnostics()[0].location.line, 3U);
}
TEST(InpSyntax, ReadsLegacyCantileverWithoutMutation) {
const auto inputPath =
repositoryRoot() / "reference" / "cantilever beam" /
"cantilever beam.inp";
const auto bytesBefore = readExactBytes(inputPath);
const auto timestampBefore = std::filesystem::last_write_time(inputPath);
const auto result = fesa::AbaqusInputReader{}.read(inputPath);
ASSERT_TRUE(result.hasValue());
EXPECT_EQ(result.value().sourceContentIdentity,
"fnv1a64:04543464cc970405");
EXPECT_EQ(result.value().sourcePath,
std::filesystem::absolute(inputPath).lexically_normal());
ASSERT_EQ(result.value().blocks.size(), 30U);
EXPECT_EQ(result.value().blocks.front().canonicalName, "HEADING");
EXPECT_EQ(result.value().blocks.front().location.line, 1U);
EXPECT_EQ(result.value().blocks.back().canonicalName, "END STEP");
const auto element = std::find_if(
result.value().blocks.begin(),
result.value().blocks.end(),
[](const fesa::KeywordBlock& block) {
return block.canonicalName == "ELEMENT";
});
ASSERT_NE(element, result.value().blocks.end());
ASSERT_EQ(element->parameters.size(), 1U);
EXPECT_EQ(element->parameters[0].name, "TYPE");
ASSERT_TRUE(element->parameters[0].value.has_value());
EXPECT_EQ(*element->parameters[0].value, "B33");
EXPECT_EQ(element->data.size(), 10U);
EXPECT_EQ(readExactBytes(inputPath), bytesBefore);
EXPECT_EQ(std::filesystem::last_write_time(inputPath), timestampBefore);
}
@@ -0,0 +1,95 @@
#include "fesa/io/abaqus/input_reader.hpp"
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
class TemporaryInputFile {
public:
TemporaryInputFile(const std::string& stem, const std::string& content)
: path_{std::filesystem::temp_directory_path() /
("fesa-" + stem + ".inp")} {
std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
if (!stream) {
throw std::runtime_error{"Unable to create INP syntax test fixture."};
}
}
~TemporaryInputFile() {
std::error_code error;
std::filesystem::remove(path_, error);
}
const std::filesystem::path& path() const noexcept {
return path_;
}
private:
std::filesystem::path path_;
};
} // namespace
TEST(InpSyntax, CanonicalizesKeywordAndParameterNamesOnly) {
const std::string originalLine =
" *eLeMeNt, TyPe= b33 , generate, ELSET=Beam_Set ";
const TemporaryInputFile input{
"canonical-names", originalLine + "\n"};
const auto result = fesa::AbaqusInputReader{}.read(input.path());
ASSERT_TRUE(result.hasValue());
ASSERT_EQ(result.value().blocks.size(), 1U);
const auto& block = result.value().blocks[0];
EXPECT_EQ(block.canonicalName, "ELEMENT");
EXPECT_EQ(block.originalLine, originalLine);
EXPECT_EQ(block.location.line, 1U);
ASSERT_EQ(block.parameters.size(), 3U);
EXPECT_EQ(block.parameters[0].name, "TYPE");
ASSERT_TRUE(block.parameters[0].value.has_value());
EXPECT_EQ(*block.parameters[0].value, "b33");
EXPECT_EQ(block.parameters[1].name, "GENERATE");
EXPECT_FALSE(block.parameters[1].value.has_value());
EXPECT_EQ(block.parameters[2].name, "ELSET");
ASSERT_TRUE(block.parameters[2].value.has_value());
EXPECT_EQ(*block.parameters[2].value, "Beam_Set");
}
TEST(InpSyntax, PreservesDataAndSourceLocations) {
const std::string exactBytes =
"** retained only in line accounting\r\n"
"\r\n"
"*NoDe\r\n"
" 0007, Label_A, ,\r\n";
const TemporaryInputFile input{"data-and-locations", exactBytes};
const auto result = fesa::AbaqusInputReader{}.read(input.path());
ASSERT_TRUE(result.hasValue());
EXPECT_EQ(
result.value().sourcePath,
std::filesystem::absolute(input.path()).lexically_normal());
EXPECT_EQ(result.value().sourceContentIdentity,
"fnv1a64:c120b6ed2445be46");
ASSERT_EQ(result.value().blocks.size(), 1U);
const auto& block = result.value().blocks[0];
EXPECT_EQ(block.canonicalName, "NODE");
EXPECT_EQ(block.originalLine, "*NoDe");
EXPECT_EQ(block.location.file, result.value().sourcePath);
EXPECT_EQ(block.location.line, 3U);
ASSERT_EQ(block.data.size(), 1U);
EXPECT_EQ(
block.data[0].fields,
(std::vector<std::string>{"0007", "Label_A", "", ""}));
EXPECT_EQ(block.data[0].location.file, result.value().sourcePath);
EXPECT_EQ(block.data[0].location.line, 4U);
}