feat(linear-static-3d-euler-beam): step 11 - inp-syntax-parser
This commit is contained in:
@@ -189,3 +189,52 @@
|
||||
- handoff: backend-free `EntityIndex`, semantic model records,
|
||||
`ModelDefinition`, and immutable `Domain::create`/const accessors are
|
||||
available to Step 11 syntax parsing and Step 12 semantic mapping.
|
||||
|
||||
## Step 11 — inp-syntax-parser
|
||||
|
||||
- task_id: `TASK-11`
|
||||
- status: `completed`
|
||||
- changed_files: `include/fesa/io/abaqus/input_syntax.hpp`,
|
||||
`include/fesa/io/abaqus/input_reader.hpp`,
|
||||
`src/fesa/io/abaqus/input_reader.cpp`,
|
||||
`tests/unit/io/abaqus/input_syntax_test.cpp`,
|
||||
`tests/unit/io/abaqus/input_reader_test.cpp`, `src/fesa/CMakeLists.txt`,
|
||||
`tests/CMakeLists.txt`,
|
||||
`docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md`,
|
||||
`phases/linear-static-3d-euler-beam/index.json`,
|
||||
`.superpowers/sdd/linear-static-3d-euler-beam/task-11-report.md`
|
||||
- requirement_ids: `FESA-REQ-LS3DEB-010`, `FESA-REQ-LS3DEB-034`,
|
||||
`FESA-REQ-LS3DEB-040`
|
||||
- test_ids: `T11-SYN-001`, `T11-SYN-002`, `T11-SYN-003`, `T11-SYN-004`
|
||||
|
||||
| stage | exact command | exit_code | expected_or_observed_result | evidence_tail |
|
||||
| --- | --- | ---: | --- | --- |
|
||||
| RED | `cmake --build .harness/build --config Debug --target fesa_tests`; `ctest --test-dir .harness/build -C Debug -R InpSyntax --output-on-failure` | 1 | Both planned test files are registered before production and fail for the missing reader API | MSVC C1083 for `fesa/io/abaqus/input_reader.hpp` in both new test translation units; CTest subsequently reported `No tests were found` because the executable could not build |
|
||||
| GREEN-build | `cmake --build .harness/build --config Debug --target fesa_tests` | 0 | Minimal syntax records, binary reader, and four tests compile and link | `input_reader.cpp`, both parser test files, `fesa_solver.lib`, and `fesa_unit_tests.exe` built without a FESA warning under `/W4 /WX` |
|
||||
| GREEN-test | `ctest --test-dir .harness/build -C Debug -R InpSyntax --output-on-failure` | 0 | All exact syntax parsing behaviors pass | 4/4 `InpSyntax` tests passed |
|
||||
| VERIFY-configure | `cmake -S . -B .harness/build -A x64 -DFESA_GTEST_SOURCE_DIR=C:/git/googletest "-DMKL_DIR=C:/Program Files (x86)/Intel/oneAPI/mkl/2026.1/lib/cmake/mkl" "-DTBB_DIR=C:/Program Files (x86)/Intel/oneAPI/tbb/2023.1/lib/cmake/tbb" "-DHDF5_DIR=C:/Program Files/HDF_Group/HDF5/2.1.1/cmake"` | 0 | Approved MSVC x64 build tree regenerates with explicit dependencies | Visual Studio 18 2026/MSVC environment selected; configure and generate completed; oneMKL 2026.1 resolved |
|
||||
| VERIFY-build | `cmake --build .harness/build --config Debug` | 0 | Full Debug build passes without a new FESA warning | `fesa_solver.lib` and `fesa_unit_tests.exe` built under `/W4 /WX` |
|
||||
| VERIFY-targeted | `ctest --test-dir .harness/build -C Debug -R InpSyntax --output-on-failure` | 0 | Focused Step 11 suite remains green | 4/4 exact `InpSyntax` tests passed |
|
||||
| VERIFY-discovery | `ctest --test-dir .harness/build -C Debug --show-only=json-v1` | 0 | CTest discovers the accumulated suite and all four exact syntax tests | 14 tests discovered with all four `InpSyntax` names and feature/unit labels |
|
||||
| VERIFY-full | `ctest --test-dir .harness/build -C Debug --output-on-failure` | 0 | Full accumulated C++ suite has zero failures | 14/14 tests passed |
|
||||
| VERIFY-dependency-direction | Backend, upward I/O, core-to-I/O, and semantic-policy scans over the Step 11 public/production files using `rg -n` fail-on-match wrappers | 0 | Public headers retain backend-free `core -> io/abaqus` direction and syntax parsing contains no Step 12 policy | backend leaks 0; upward I/O dependencies 0; core-to-I/O dependencies 0; semantic-policy matches 0; exactly four `InpSyntax` tests found |
|
||||
| VERIFY-diff | `git diff --check` | 0 | Patch has no whitespace errors | Exit 0; only informational Git LF-to-CRLF working-copy notices were emitted |
|
||||
| VERIFY-reference | `git diff --exit-code -- reference/`; `git status --short -- reference/` | 0 | Approved legacy reference artifacts remain unchanged | Diff exit 0 and reference status empty |
|
||||
|
||||
- contract_checks: `ParsedInput` records the absolute lexically normalized
|
||||
source path and exact binary-byte FNV-1a identity formatted as
|
||||
`fnv1a64:` plus 16 lowercase hexadecimal digits. Keyword and parameter names
|
||||
alone become ASCII uppercase; parameter values, data label lexemes, empty
|
||||
and trailing data fields, and each keyword `originalLine` preserve source
|
||||
spelling. Comments and blanks remain excluded while physical 1-based file
|
||||
line accounting is retained. Unreadable file, empty keyword, and orphan data
|
||||
return categorized input diagnostics. No keyword allowlist, wrapper/nesting,
|
||||
numeric conversion, B31/B33, or output-request decision exists in this Step.
|
||||
The read-only legacy input parses as 30 keyword blocks with exact identity
|
||||
`fnv1a64:04543464cc970405`, unchanged bytes, and unchanged modification time.
|
||||
- generated_evidence: `.harness/build/src/fesa/Debug/fesa_solver.lib`,
|
||||
`.harness/build/tests/Debug/fesa_unit_tests.exe`
|
||||
- reference_diff: unchanged; `git diff --exit-code -- reference/` exit 0
|
||||
- handoff: backend-free `KeywordParameter`, `DataLine`, `KeywordBlock`,
|
||||
`ParsedInput`, and `AbaqusInputReader::read` with syntax/source provenance are
|
||||
available to Step 12 semantic mapping.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "fesa/core/status.hpp"
|
||||
#include "fesa/io/abaqus/input_syntax.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
// Reads only physical keyword/data/comment syntax; semantic policy is applied
|
||||
// later by AbaqusDomainMapper.
|
||||
class AbaqusInputReader {
|
||||
public:
|
||||
Result<ParsedInput> read(const std::filesystem::path& inputPath) const;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "fesa/core/source_identity.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
// Names are canonicalized for syntax lookup while values remain source text.
|
||||
struct KeywordParameter {
|
||||
std::string name;
|
||||
std::optional<std::string> value;
|
||||
};
|
||||
|
||||
struct DataLine {
|
||||
std::vector<std::string> fields;
|
||||
SourceLocation location;
|
||||
};
|
||||
|
||||
struct KeywordBlock {
|
||||
std::string canonicalName;
|
||||
std::string originalLine;
|
||||
std::vector<KeywordParameter> parameters;
|
||||
std::vector<DataLine> data;
|
||||
SourceLocation location;
|
||||
};
|
||||
|
||||
struct ParsedInput {
|
||||
std::filesystem::path sourcePath;
|
||||
std::string sourceContentIdentity;
|
||||
std::vector<KeywordBlock> blocks;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
@@ -4,6 +4,7 @@ add_library(
|
||||
build_info.cpp
|
||||
core/diagnostic.cpp
|
||||
core/status.cpp
|
||||
io/abaqus/input_reader.cpp
|
||||
math/matrix.cpp
|
||||
math/vector.cpp
|
||||
model/domain.cpp
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
#include "fesa/io/abaqus/input_reader.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path normalizedPath(const std::filesystem::path& path) {
|
||||
std::error_code error;
|
||||
const auto absolute = std::filesystem::absolute(path, error);
|
||||
return (error ? path : absolute).lexically_normal();
|
||||
}
|
||||
|
||||
bool isAsciiWhitespace(char value) noexcept {
|
||||
return value == ' ' || value == '\t' || value == '\r' ||
|
||||
value == '\n' || value == '\f' || value == '\v';
|
||||
}
|
||||
|
||||
std::string trim(std::string_view text) {
|
||||
while (!text.empty() && isAsciiWhitespace(text.front())) {
|
||||
text.remove_prefix(1U);
|
||||
}
|
||||
while (!text.empty() && isAsciiWhitespace(text.back())) {
|
||||
text.remove_suffix(1U);
|
||||
}
|
||||
return std::string{text};
|
||||
}
|
||||
|
||||
std::string uppercaseAscii(std::string value) {
|
||||
std::transform(
|
||||
value.begin(), value.end(), value.begin(), [](char character) {
|
||||
if (character >= 'a' && character <= 'z') {
|
||||
return static_cast<char>(character - 'a' + 'A');
|
||||
}
|
||||
return character;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::vector<std::string> splitFields(std::string_view line) {
|
||||
std::vector<std::string> fields;
|
||||
std::size_t fieldStart = 0U;
|
||||
while (true) {
|
||||
const std::size_t separator = line.find(',', fieldStart);
|
||||
if (separator == std::string_view::npos) {
|
||||
fields.push_back(trim(line.substr(fieldStart)));
|
||||
break;
|
||||
}
|
||||
fields.push_back(trim(line.substr(fieldStart, separator - fieldStart)));
|
||||
fieldStart = separator + 1U;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
std::string contentIdentity(const std::string& bytes) {
|
||||
constexpr std::uint64_t offsetBasis = 14695981039346656037ULL;
|
||||
constexpr std::uint64_t prime = 1099511628211ULL;
|
||||
std::uint64_t hash = offsetBasis;
|
||||
// Hash the binary input before CRLF handling so provenance follows the
|
||||
// exact file bytes rather than a normalized text representation.
|
||||
for (const unsigned char byte : bytes) {
|
||||
hash ^= static_cast<std::uint64_t>(byte);
|
||||
hash *= prime;
|
||||
}
|
||||
|
||||
std::ostringstream formatted;
|
||||
formatted << "fnv1a64:" << std::hex << std::setfill('0')
|
||||
<< std::setw(16) << hash;
|
||||
return formatted.str();
|
||||
}
|
||||
|
||||
Result<ParsedInput> failure(
|
||||
const std::filesystem::path& sourcePath,
|
||||
std::size_t line,
|
||||
std::string code,
|
||||
std::string keyword,
|
||||
std::string message) {
|
||||
Diagnostic diagnostic{
|
||||
Severity::error,
|
||||
std::move(code),
|
||||
{sourcePath, line},
|
||||
std::move(keyword),
|
||||
"",
|
||||
std::move(message)};
|
||||
return Result<ParsedInput>::failure(Status::failure(
|
||||
FailureCategory::input, {std::move(diagnostic)}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result<ParsedInput> AbaqusInputReader::read(
|
||||
const std::filesystem::path& inputPath) const {
|
||||
const auto sourcePath = normalizedPath(inputPath);
|
||||
std::ifstream stream{sourcePath, std::ios::binary};
|
||||
if (!stream) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
0U,
|
||||
"input-file-unreadable",
|
||||
"",
|
||||
"The Abaqus input file could not be opened for reading.");
|
||||
}
|
||||
|
||||
const std::string bytes{
|
||||
std::istreambuf_iterator<char>{stream},
|
||||
std::istreambuf_iterator<char>{}};
|
||||
if (stream.bad()) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
0U,
|
||||
"input-file-unreadable",
|
||||
"",
|
||||
"The Abaqus input file could not be read completely.");
|
||||
}
|
||||
|
||||
ParsedInput parsed{sourcePath, contentIdentity(bytes), {}};
|
||||
std::size_t lineStart = 0U;
|
||||
std::size_t lineNumber = 1U;
|
||||
while (lineStart < bytes.size()) {
|
||||
const std::size_t newline = bytes.find('\n', lineStart);
|
||||
const std::size_t lineEnd =
|
||||
newline == std::string::npos ? bytes.size() : newline;
|
||||
std::string originalLine = bytes.substr(lineStart, lineEnd - lineStart);
|
||||
if (!originalLine.empty() && originalLine.back() == '\r') {
|
||||
originalLine.pop_back();
|
||||
}
|
||||
|
||||
const std::string trimmedLine = trim(originalLine);
|
||||
if (!trimmedLine.empty() && trimmedLine.rfind("**", 0U) != 0U) {
|
||||
if (trimmedLine.front() == '*') {
|
||||
const auto fields = splitFields(trimmedLine);
|
||||
const std::string keywordText =
|
||||
fields.empty() ? std::string{} : trim(
|
||||
std::string_view{fields[0]}.substr(1U));
|
||||
if (keywordText.empty()) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
lineNumber,
|
||||
"malformed-keyword",
|
||||
trimmedLine,
|
||||
"A keyword line requires a non-empty keyword name.");
|
||||
}
|
||||
|
||||
KeywordBlock block{
|
||||
uppercaseAscii(keywordText),
|
||||
originalLine,
|
||||
{},
|
||||
{},
|
||||
{sourcePath, lineNumber}};
|
||||
for (std::size_t index = 1U; index < fields.size(); ++index) {
|
||||
const std::string& field = fields[index];
|
||||
if (field.empty()) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
lineNumber,
|
||||
"malformed-keyword",
|
||||
block.canonicalName,
|
||||
"A keyword parameter name cannot be empty.");
|
||||
}
|
||||
|
||||
const std::size_t equals = field.find('=');
|
||||
const std::string parameterName = trim(std::string_view{field}.substr(
|
||||
0U, equals));
|
||||
if (parameterName.empty()) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
lineNumber,
|
||||
"malformed-keyword",
|
||||
block.canonicalName,
|
||||
"A keyword parameter name cannot be empty.");
|
||||
}
|
||||
|
||||
KeywordParameter parameter{
|
||||
uppercaseAscii(parameterName), std::nullopt};
|
||||
if (equals != std::string::npos) {
|
||||
parameter.value = trim(
|
||||
std::string_view{field}.substr(equals + 1U));
|
||||
}
|
||||
block.parameters.push_back(std::move(parameter));
|
||||
}
|
||||
parsed.blocks.push_back(std::move(block));
|
||||
} else {
|
||||
if (parsed.blocks.empty()) {
|
||||
return failure(
|
||||
sourcePath,
|
||||
lineNumber,
|
||||
"orphan-data-line",
|
||||
"",
|
||||
"A data line must follow a keyword line.");
|
||||
}
|
||||
parsed.blocks.back().data.push_back(
|
||||
{splitFields(originalLine), {sourcePath, lineNumber}});
|
||||
}
|
||||
}
|
||||
|
||||
if (newline == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
lineStart = newline + 1U;
|
||||
++lineNumber;
|
||||
}
|
||||
|
||||
return Result<ParsedInput>::success(std::move(parsed));
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user