feat(cpp-object-oriented-modular-refactoring): step 6 - io-application-google-style

This commit is contained in:
KOKO\Mimi
2026-08-16 06:59:50 +09:00
parent bb178c9d3c
commit 83fd1d1c7e
31 changed files with 10499 additions and 11445 deletions
+21
View File
@@ -0,0 +1,21 @@
#ifndef FESA_APP_FESA_APPLICATION_H_
#define FESA_APP_FESA_APPLICATION_H_
#include <string>
#include <vector>
namespace fesa {
/// @brief Owns the argv-independent CLI contract and stable process exit codes.
class FesaApplication {
public:
/// @brief Runs one solver invocation from operands and options after argv[0].
/// @param arguments Input path and optional `--output` pair.
/// @return The stable CLI exit code for usage, input, model, solver, or
/// output status.
int Run(const std::vector<std::string>& arguments);
};
} // namespace fesa
#endif // FESA_APP_FESA_APPLICATION_H_
-14
View File
@@ -1,14 +0,0 @@
#pragma once
#include <string>
#include <vector>
namespace fesa {
// Owns the argv-independent command-line contract and stable process codes.
class FesaApplication {
public:
int run(const std::vector<std::string>& arguments);
};
} // namespace fesa
+24
View File
@@ -0,0 +1,24 @@
#ifndef FESA_IO_ABAQUS_DOMAIN_MAPPER_H_
#define FESA_IO_ABAQUS_DOMAIN_MAPPER_H_
#include "fesa/core/status.h"
#include "fesa/io/abaqus/input_syntax.h"
#include "fesa/model/domain.h"
namespace fesa {
/// @brief Maps syntax-only blocks into an approved immutable semantic model.
/// @note Source identity and declaration order are preserved through mapping.
class AbaqusDomainMapper {
public:
/// @brief Resolves supported Abaqus syntax into a complete Domain candidate.
/// @param input Parsed syntax whose source locations remain valid for
/// mapping.
/// @return A committed Domain or structured input/model diagnostics; partial
/// domains are never returned.
Result<Domain> Map(const ParsedInput& input) const;
};
} // namespace fesa
#endif // FESA_IO_ABAQUS_DOMAIN_MAPPER_H_
-15
View File
@@ -1,15 +0,0 @@
#pragma once
#include "fesa/core/status.h"
#include "fesa/io/abaqus/input_syntax.hpp"
#include "fesa/model/domain.h"
namespace fesa {
// Converts syntax-only blocks into the approved immutable B33 semantic model.
class AbaqusDomainMapper {
public:
Result<Domain> map(const ParsedInput& input) const;
};
} // namespace fesa
+24
View File
@@ -0,0 +1,24 @@
#ifndef FESA_IO_ABAQUS_INPUT_READER_H_
#define FESA_IO_ABAQUS_INPUT_READER_H_
#include <filesystem>
#include "fesa/core/status.h"
#include "fesa/io/abaqus/input_syntax.h"
namespace fesa {
/// @brief Reads Abaqus physical keyword, data, and comment syntax.
/// @note Semantic policy is applied later by AbaqusDomainMapper.
class AbaqusInputReader {
public:
/// @brief Parses one input file without applying semantic mapping policy.
/// @param input_path Path to the exact source bytes whose identity is
/// retained.
/// @return Parsed syntax or a structured input diagnostic.
Result<ParsedInput> Read(const std::filesystem::path& input_path) const;
};
} // namespace fesa
#endif // FESA_IO_ABAQUS_INPUT_READER_H_
-17
View File
@@ -1,17 +0,0 @@
#pragma once
#include "fesa/core/status.h"
#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
+46
View File
@@ -0,0 +1,46 @@
#ifndef FESA_IO_ABAQUS_INPUT_SYNTAX_H_
#define FESA_IO_ABAQUS_INPUT_SYNTAX_H_
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
#include "fesa/core/source_identity.h"
namespace fesa {
/// @brief Stores a canonical parameter name and its optional source value.
/// @note Parameter names are canonicalized for lookup while values remain
/// source text.
struct KeywordParameter {
std::string name;
std::optional<std::string> value;
};
/// @brief Stores one parsed data row with its source location.
struct DataLine {
std::vector<std::string> fields;
SourceLocation location;
};
/// @brief Stores one syntax-only keyword block and its following data rows.
struct KeywordBlock {
std::string canonical_name;
std::string original_line;
std::vector<KeywordParameter> parameters;
std::vector<DataLine> data;
SourceLocation location;
};
/// @brief Stores the parsed syntax and stable identity of one Abaqus input
/// file.
struct ParsedInput {
std::filesystem::path source_path;
std::string source_content_identity;
std::vector<KeywordBlock> blocks;
};
} // namespace fesa
#endif // FESA_IO_ABAQUS_INPUT_SYNTAX_H_
-37
View File
@@ -1,37 +0,0 @@
#pragma once
#include "fesa/core/source_identity.h"
#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
@@ -0,0 +1,28 @@
#ifndef FESA_IO_HDF5_HDF5_RESULTS_WRITER_H_
#define FESA_IO_HDF5_HDF5_RESULTS_WRITER_H_
#include "fesa/results/results_writer.h"
namespace fesa {
/// @brief Writes authoritative schema-v0 HDF5 results atomically.
/// @note HDF5 and platform types remain private to the implementation.
class Hdf5ResultsWriter final : public ResultsWriter {
public:
/// @brief Writes and self-checks a complete candidate before finalization.
/// @param output_path Final authoritative path; the candidate is created in
/// the same directory.
/// @param domain Immutable source and model identity.
/// @param state Fully recovered analysis state.
/// @param diagnostics Deterministically ordered run diagnostics.
/// @return Success only after atomic replacement or a structured output
/// failure.
/// @note A failed candidate does not replace an existing valid final file.
Status Write(const std::filesystem::path& output_path, const Domain& domain,
const AnalysisState& state,
const std::vector<Diagnostic>& diagnostics) override;
};
} // namespace fesa
#endif // FESA_IO_HDF5_HDF5_RESULTS_WRITER_H_
@@ -1,17 +0,0 @@
#pragma once
#include "fesa/results/results_writer.h"
namespace fesa {
// Writes schema-v0 output while keeping backend and platform types private.
class Hdf5ResultsWriter final : public ResultsWriter {
public:
Status Write(
const std::filesystem::path& outputPath,
const Domain& domain,
const AnalysisState& state,
const std::vector<Diagnostic>& diagnostics) override;
};
} // namespace fesa
+4 -4
View File
@@ -5,8 +5,8 @@
#include "fesa/assembly/load_assembler.h" #include "fesa/assembly/load_assembler.h"
#include "fesa/assembly/parallel_for.h" #include "fesa/assembly/parallel_for.h"
#include "fesa/assembly/sparse_assembler.h" #include "fesa/assembly/sparse_assembler.h"
#include "fesa/io/abaqus/domain_mapper.hpp" #include "fesa/io/abaqus/domain_mapper.h"
#include "fesa/io/abaqus/input_reader.hpp" #include "fesa/io/abaqus/input_reader.h"
#include "fesa/results/result_recovery.h" #include "fesa/results/result_recovery.h"
#include "fesa/results/results_writer.h" #include "fesa/results/results_writer.h"
#include "fesa/solvers/linear/linear_solver.h" #include "fesa/solvers/linear/linear_solver.h"
@@ -65,11 +65,11 @@ Status LinearStaticAnalysis::Initialize(const AnalysisRequest& request) {
diagnostics_.clear(); diagnostics_.clear();
request_ = request; request_ = request;
const auto parsed = AbaqusInputReader{}.read(request_.input_path); const auto parsed = AbaqusInputReader{}.Read(request_.input_path);
if (!parsed.HasValue()) { if (!parsed.HasValue()) {
return parsed.GetStatus(); return parsed.GetStatus();
} }
auto domain = AbaqusDomainMapper{}.map(parsed.Value()); auto domain = AbaqusDomainMapper{}.Map(parsed.Value());
if (!domain.HasValue()) { if (!domain.HasValue()) {
return domain.GetStatus(); return domain.GetStatus();
} }
+68 -77
View File
@@ -1,16 +1,16 @@
#include "fesa/app/fesa_application.hpp" #include "fesa/app/fesa_application.h"
#include "fesa/analysis/linear_static_analysis.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/core/diagnostic.h"
#include "fesa/io/hdf5/hdf5_results_writer.hpp"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include <filesystem> #include <filesystem>
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <vector> #include <vector>
#include "fesa/analysis/linear_static_analysis.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/core/diagnostic.h"
#include "fesa/io/hdf5/hdf5_results_writer.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
namespace fesa { namespace fesa {
namespace { namespace {
@@ -21,93 +21,84 @@ constexpr int kModelExitCode = 4;
constexpr int kSolverExitCode = 5; constexpr int kSolverExitCode = 5;
constexpr int kOutputExitCode = 6; constexpr int kOutputExitCode = 6;
bool startsWithOption(const std::string& argument) { bool StartsWithOption(const std::string& argument) {
return !argument.empty() && argument.front() == '-'; return !argument.empty() && argument.front() == '-';
} }
Diagnostic usageDiagnostic() { Diagnostic UsageDiagnostic() {
return { return {Severity::kError,
Severity::kError, "cli-usage",
"cli-usage", {{}, 0U},
{{}, 0U}, "",
"", "",
"", "Usage: fesa.exe <model.inp> [--output <results.h5>]."};
"Usage: fesa.exe <model.inp> [--output <results.h5>]."};
} }
const char* severityName(const Severity severity) { const char* SeverityName(const Severity severity) {
return severity == Severity::kWarning ? "warning" : "error"; return severity == Severity::kWarning ? "warning" : "error";
} }
void writeDiagnostics(std::vector<Diagnostic> diagnostics) { void WriteDiagnostics(std::vector<Diagnostic> diagnostics) {
SortDiagnostics(diagnostics); SortDiagnostics(diagnostics);
for (const auto& diagnostic : diagnostics) { for (const auto& diagnostic : diagnostics) {
// Stable field labels and tab separators keep empty source fields // Stable field labels and tab separators keep empty source fields
// explicit without depending on locale-specific formatting. // explicit without depending on locale-specific formatting.
std::cerr std::cerr << "severity=" << SeverityName(diagnostic.severity) << '\t'
<< "severity=" << severityName(diagnostic.severity) << "code=" << diagnostic.code << '\t'
<< '\t' << "code=" << diagnostic.code << "file=" << diagnostic.location.file.generic_u8string() << '\t'
<< '\t' << "file=" << "line=" << diagnostic.location.line << '\t'
<< diagnostic.location.file.generic_u8string() << "keyword=" << diagnostic.keyword << '\t'
<< '\t' << "line=" << diagnostic.location.line << "entity_identity=" << diagnostic.entity_identity << '\t'
<< '\t' << "keyword=" << diagnostic.keyword << "message=" << diagnostic.message << '\n';
<< '\t' << "entity_identity=" << diagnostic.entity_identity }
<< '\t' << "message=" << diagnostic.message
<< '\n';
}
} }
int exitCodeFor(const Status& status) { int ExitCodeFor(const Status& status) {
switch (status.Category().value_or(FailureCategory::kInput)) { switch (status.Category().value_or(FailureCategory::kInput)) {
case FailureCategory::kInput: case FailureCategory::kInput:
return kInputExitCode; return kInputExitCode;
case FailureCategory::kModel: case FailureCategory::kModel:
return kModelExitCode; return kModelExitCode;
case FailureCategory::kSolver: case FailureCategory::kSolver:
return kSolverExitCode; return kSolverExitCode;
case FailureCategory::kOutput: case FailureCategory::kOutput:
return kOutputExitCode; return kOutputExitCode;
} }
return kInputExitCode; return kInputExitCode;
} }
} // namespace } // namespace
int FesaApplication::run(const std::vector<std::string>& arguments) { int FesaApplication::Run(const std::vector<std::string>& arguments) {
const bool defaultOutputForm = const bool default_output_form = arguments.size() == 1U &&
arguments.size() == 1U && !arguments[0U].empty() &&
!arguments[0U].empty() && !StartsWithOption(arguments[0U]);
!startsWithOption(arguments[0U]); const bool explicit_output_form =
const bool explicitOutputForm = arguments.size() == 3U && !arguments[0U].empty() &&
arguments.size() == 3U && !StartsWithOption(arguments[0U]) && arguments[1U] == "--output" &&
!arguments[0U].empty() && !arguments[2U].empty() && !StartsWithOption(arguments[2U]);
!startsWithOption(arguments[0U]) && if (!default_output_form && !explicit_output_form) {
arguments[1U] == "--output" && WriteDiagnostics({UsageDiagnostic()});
!arguments[2U].empty() && return kUsageExitCode;
!startsWithOption(arguments[2U]); }
if (!defaultOutputForm && !explicitOutputForm) {
writeDiagnostics({usageDiagnostic()});
return kUsageExitCode;
}
AnalysisRequest request; AnalysisRequest request;
request.input_path = arguments[0U]; request.input_path = arguments[0U];
request.output_path = explicitOutputForm request.output_path = explicit_output_form
? std::filesystem::path{arguments[2U]} ? std::filesystem::path{arguments[2U]}
: std::filesystem::current_path() / "results.h5"; : std::filesystem::current_path() / "results.h5";
TbbParallelFor parallelFor; TbbParallelFor parallel_for;
MklPardisoSolver linearSolver; MklPardisoSolver linear_solver;
Hdf5ResultsWriter resultsWriter; Hdf5ResultsWriter results_writer;
LinearStaticAnalysis analysis{ LinearStaticAnalysis analysis{parallel_for, linear_solver, results_writer};
parallelFor, linearSolver, resultsWriter}; const Status status = analysis.Run(request);
const Status status = analysis.Run(request); if (status.IsOk()) {
if (status.IsOk()) { return kSuccessExitCode;
return kSuccessExitCode; }
}
writeDiagnostics(status.Diagnostics()); WriteDiagnostics(status.Diagnostics());
return exitCodeFor(status); return ExitCodeFor(status);
} }
} // namespace fesa } // namespace fesa
+11 -11
View File
@@ -1,17 +1,17 @@
#include "fesa/app/fesa_application.hpp"
#include <cstddef> #include <cstddef>
#include <string> #include <string>
#include <vector> #include <vector>
#include "fesa/app/fesa_application.h"
int main(const int argc, char* argv[]) { int main(const int argc, char* argv[]) {
std::vector<std::string> arguments; std::vector<std::string> arguments;
if (argc > 1) { if (argc > 1) {
arguments.reserve(static_cast<std::size_t>(argc - 1)); arguments.reserve(static_cast<std::size_t>(argc - 1));
} }
// The application boundary receives only operands and options, not argv[0]. // The application boundary receives only operands and options, not argv[0].
for (int index = 1; index < argc; ++index) { for (int index = 1; index < argc; ++index) {
arguments.emplace_back(argv[index]); arguments.emplace_back(argv[index]);
} }
return fesa::FesaApplication{}.run(arguments); return fesa::FesaApplication{}.Run(arguments);
} }
File diff suppressed because it is too large Load Diff
+160 -186
View File
@@ -1,4 +1,4 @@
#include "fesa/io/abaqus/input_reader.hpp" #include "fesa/io/abaqus/input_reader.h"
#include <algorithm> #include <algorithm>
#include <cstdint> #include <cstdint>
@@ -15,202 +15,176 @@
namespace fesa { namespace fesa {
namespace { namespace {
std::filesystem::path normalizedPath(const std::filesystem::path& path) { std::filesystem::path NormalizedPath(const std::filesystem::path& path) {
std::error_code error; std::error_code error;
const auto absolute = std::filesystem::absolute(path, error); const auto absolute = std::filesystem::absolute(path, error);
return (error ? path : absolute).lexically_normal(); return (error ? path : absolute).lexically_normal();
} }
bool isAsciiWhitespace(char value) noexcept { bool IsAsciiWhitespace(char value) noexcept {
return value == ' ' || value == '\t' || value == '\r' || return value == ' ' || value == '\t' || value == '\r' || value == '\n' ||
value == '\n' || value == '\f' || value == '\v'; value == '\f' || value == '\v';
} }
std::string trim(std::string_view text) { std::string Trim(std::string_view text) {
while (!text.empty() && isAsciiWhitespace(text.front())) { while (!text.empty() && IsAsciiWhitespace(text.front())) {
text.remove_prefix(1U); 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');
} }
while (!text.empty() && isAsciiWhitespace(text.back())) { return character;
text.remove_suffix(1U); });
} return value;
return std::string{text};
} }
std::string uppercaseAscii(std::string value) { std::vector<std::string> SplitFields(std::string_view line) {
std::transform( std::vector<std::string> fields;
value.begin(), value.end(), value.begin(), [](char character) { std::size_t field_start = 0U;
if (character >= 'a' && character <= 'z') { while (true) {
return static_cast<char>(character - 'a' + 'A'); const std::size_t separator = line.find(',', field_start);
} if (separator == std::string_view::npos) {
return character; fields.push_back(Trim(line.substr(field_start)));
}); break;
return value; }
fields.push_back(Trim(line.substr(field_start, separator - field_start)));
field_start = separator + 1U;
}
return fields;
} }
std::vector<std::string> splitFields(std::string_view line) { /// @brief Computes the stable identity of the exact source bytes.
std::vector<std::string> fields; /// @note This runs before line-ending handling so parser provenance is not
std::size_t fieldStart = 0U; /// affected by text normalization.
while (true) { std::string ContentIdentity(const std::string& bytes) {
const std::size_t separator = line.find(',', fieldStart); constexpr std::uint64_t kOffsetBasis = 14695981039346656037ULL;
if (separator == std::string_view::npos) { constexpr std::uint64_t kPrime = 1099511628211ULL;
fields.push_back(trim(line.substr(fieldStart))); std::uint64_t hash = kOffsetBasis;
break; // Hash the binary input before CRLF handling so provenance follows the
} // exact file bytes rather than a normalized text representation.
fields.push_back(trim(line.substr(fieldStart, separator - fieldStart))); for (const unsigned char byte : bytes) {
fieldStart = separator + 1U; hash ^= static_cast<std::uint64_t>(byte);
} hash *= kPrime;
return fields; }
std::ostringstream formatted;
formatted << "fnv1a64:" << std::hex << std::setfill('0') << std::setw(16)
<< hash;
return formatted.str();
} }
std::string contentIdentity(const std::string& bytes) { Result<ParsedInput> Failure(const std::filesystem::path& source_path,
constexpr std::uint64_t offsetBasis = 14695981039346656037ULL; std::size_t line, std::string code,
constexpr std::uint64_t prime = 1099511628211ULL; std::string keyword, std::string message) {
std::uint64_t hash = offsetBasis; Diagnostic diagnostic{Severity::kError,
// Hash the binary input before CRLF handling so provenance follows the std::move(code),
// exact file bytes rather than a normalized text representation. {source_path, line},
for (const unsigned char byte : bytes) { std::move(keyword),
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::kError,
std::move(code),
{sourcePath, line},
std::move(keyword),
"",
std::move(message)};
return Result<ParsedInput>::Failure(Status::Failure(
FailureCategory::kInput, {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."); std::move(message)};
} return Result<ParsedInput>::Failure(
parsed.blocks.back().data.push_back( Status::Failure(FailureCategory::kInput, {std::move(diagnostic)}));
{splitFields(originalLine), {sourcePath, lineNumber}});
}
}
if (newline == std::string::npos) {
break;
}
lineStart = newline + 1U;
++lineNumber;
}
return Result<ParsedInput>::Success(std::move(parsed));
} }
} // namespace fesa } // namespace
Result<ParsedInput> AbaqusInputReader::Read(
const std::filesystem::path& input_path) const {
const auto source_path = NormalizedPath(input_path);
std::ifstream stream{source_path, std::ios::binary};
if (!stream) {
return Failure(source_path, 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(source_path, 0U, "input-file-unreadable", "",
"The Abaqus input file could not be read completely.");
}
ParsedInput parsed{source_path, ContentIdentity(bytes), {}};
std::size_t line_start = 0U;
std::size_t line_number = 1U;
while (line_start < bytes.size()) {
const std::size_t newline = bytes.find('\n', line_start);
const std::size_t line_end =
newline == std::string::npos ? bytes.size() : newline;
std::string original_line = bytes.substr(line_start, line_end - line_start);
if (!original_line.empty() && original_line.back() == '\r') {
original_line.pop_back();
}
const std::string trimmed_line = Trim(original_line);
if (!trimmed_line.empty() && trimmed_line.rfind("**", 0U) != 0U) {
if (trimmed_line.front() == '*') {
const auto fields = SplitFields(trimmed_line);
const std::string keyword_text =
fields.empty() ? std::string{}
: Trim(std::string_view{fields[0]}.substr(1U));
if (keyword_text.empty()) {
return Failure(source_path, line_number, "malformed-keyword",
trimmed_line,
"A keyword line requires a non-empty keyword name.");
}
KeywordBlock block{UppercaseAscii(keyword_text),
original_line,
{},
{},
{source_path, line_number}};
for (std::size_t index = 1U; index < fields.size(); ++index) {
const std::string& field = fields[index];
if (field.empty()) {
return Failure(source_path, line_number, "malformed-keyword",
block.canonical_name,
"A keyword parameter name cannot be empty.");
}
const std::size_t equals = field.find('=');
const std::string parameter_name =
Trim(std::string_view{field}.substr(0U, equals));
if (parameter_name.empty()) {
return Failure(source_path, line_number, "malformed-keyword",
block.canonical_name,
"A keyword parameter name cannot be empty.");
}
KeywordParameter parameter{UppercaseAscii(parameter_name),
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(source_path, line_number, "orphan-data-line", "",
"A data line must follow a keyword line.");
}
parsed.blocks.back().data.push_back(
{SplitFields(original_line), {source_path, line_number}});
}
}
if (newline == std::string::npos) {
break;
}
line_start = newline + 1U;
++line_number;
}
return Result<ParsedInput>::Success(std::move(parsed));
}
} // namespace fesa
File diff suppressed because it is too large Load Diff
+303 -329
View File
@@ -1,4 +1,4 @@
#include "fesa/app/fesa_application.hpp" #include "fesa/app/fesa_application.h"
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <hdf5.h> #include <hdf5.h>
@@ -19,101 +19,94 @@ namespace {
constexpr const char* kStepRoot = "/steps/Step-1/frames/0"; constexpr const char* kStepRoot = "/steps/Step-1/frames/0";
class TempDirectory { class TempDirectory {
public: public:
explicit TempDirectory(const std::string& label) { explicit TempDirectory(const std::string& label) {
static std::atomic<unsigned long long> sequence{0U}; static std::atomic<unsigned long long> sequence{0U};
const auto tick = std::chrono::steady_clock::now() const auto tick =
.time_since_epoch() std::chrono::steady_clock::now().time_since_epoch().count();
.count(); path_ = std::filesystem::temp_directory_path() /
path_ = std::filesystem::temp_directory_path() / ("fesa-step24-app-" + label + "-" + std::to_string(tick) + "-" +
("fesa-step24-app-" + label + "-" +
std::to_string(tick) + "-" +
std::to_string(sequence.fetch_add(1U))); std::to_string(sequence.fetch_add(1U)));
std::error_code error; std::error_code error;
if (!std::filesystem::create_directory(path_, error) || error) { if (!std::filesystem::create_directory(path_, error) || error) {
throw std::runtime_error{"Unable to create the Step 24 app fixture."}; throw std::runtime_error{"Unable to create the Step 24 app fixture."};
}
} }
}
TempDirectory(const TempDirectory&) = delete; TempDirectory(const TempDirectory&) = delete;
TempDirectory& operator=(const TempDirectory&) = delete; TempDirectory& operator=(const TempDirectory&) = delete;
~TempDirectory() { ~TempDirectory() {
std::error_code ignored; std::error_code ignored;
std::filesystem::remove_all(path_, ignored); std::filesystem::remove_all(path_, ignored);
} }
const std::filesystem::path& path() const noexcept { return path_; } const std::filesystem::path& Path() const noexcept { return path_; }
private: private:
std::filesystem::path path_; std::filesystem::path path_;
}; };
class CurrentDirectoryGuard { class CurrentDirectoryGuard {
public: public:
explicit CurrentDirectoryGuard(const std::filesystem::path& replacement) explicit CurrentDirectoryGuard(const std::filesystem::path& replacement)
: original_{std::filesystem::current_path()} { : original_{std::filesystem::current_path()} {
std::filesystem::current_path(replacement); std::filesystem::current_path(replacement);
} }
CurrentDirectoryGuard(const CurrentDirectoryGuard&) = delete; CurrentDirectoryGuard(const CurrentDirectoryGuard&) = delete;
CurrentDirectoryGuard& operator=(const CurrentDirectoryGuard&) = delete; CurrentDirectoryGuard& operator=(const CurrentDirectoryGuard&) = delete;
~CurrentDirectoryGuard() { ~CurrentDirectoryGuard() {
std::error_code ignored; std::error_code ignored;
std::filesystem::current_path(original_, ignored); std::filesystem::current_path(original_, ignored);
} }
private: private:
std::filesystem::path original_; std::filesystem::path original_;
}; };
class Hdf5Handle { class Hdf5Handle {
public: public:
using Closer = herr_t (*)(hid_t); using Closer = herr_t (*)(hid_t);
Hdf5Handle(const hid_t value, Closer closer) Hdf5Handle(const hid_t value, Closer closer)
: value_{value}, closer_{closer} {} : value_{value}, closer_{closer} {}
Hdf5Handle(const Hdf5Handle&) = delete; Hdf5Handle(const Hdf5Handle&) = delete;
Hdf5Handle& operator=(const Hdf5Handle&) = delete; Hdf5Handle& operator=(const Hdf5Handle&) = delete;
Hdf5Handle(Hdf5Handle&& other) noexcept Hdf5Handle(Hdf5Handle&& other) noexcept
: value_{other.value_}, closer_{other.closer_} { : value_{other.value_}, closer_{other.closer_} {
other.value_ = -1; other.value_ = -1;
other.closer_ = nullptr; other.closer_ = nullptr;
} }
~Hdf5Handle() { ~Hdf5Handle() {
if (value_ >= 0 && closer_ != nullptr) { if (value_ >= 0 && closer_ != nullptr) {
(void)closer_(value_); (void)closer_(value_);
}
} }
}
hid_t get() const noexcept { return value_; } hid_t Get() const noexcept { return value_; }
private: private:
hid_t value_{-1}; hid_t value_{-1};
Closer closer_{nullptr}; Closer closer_{nullptr};
}; };
void writeText(const std::filesystem::path& path, const std::string& text) { void WriteText(const std::filesystem::path& path, const std::string& text) {
std::ofstream stream{path, std::ios::binary | std::ios::trunc}; std::ofstream stream{path, std::ios::binary | std::ios::trunc};
stream.write(text.data(), static_cast<std::streamsize>(text.size())); stream.write(text.data(), static_cast<std::streamsize>(text.size()));
if (!stream) { if (!stream) {
throw std::runtime_error{"Unable to write the Step 24 app input."}; throw std::runtime_error{"Unable to write the Step 24 app input."};
} }
} }
std::string axialDeck( std::string AxialDeck(const bool constrained, const bool zero_length,
const bool constrained, const bool output_requests) {
const bool zeroLength, const std::string second_node =
const bool outputRequests) { zero_length ? "2, 0., 0., 0.\n" : "2, 2., 0., 0.\n";
const std::string secondNode = zeroLength const std::string boundaries =
? "2, 0., 0., 0.\n" constrained ? "*Boundary\nRoot, 1, 6\nTip, 2, 6\n" : "";
: "2, 2., 0., 0.\n"; const std::string outputs = output_requests ? R"inp(*Output, field
const std::string boundaries = constrained
? "*Boundary\nRoot, 1, 6\nTip, 2, 6\n"
: "";
const std::string outputs = outputRequests
? R"inp(*Output, field
*Node Output *Node Output
U, RF U, RF
*Element Output, directions=YES *Element Output, directions=YES
@@ -121,12 +114,13 @@ S, SF
*Output, history *Output, history
*Contact Output *Contact Output
)inp" )inp"
: ""; : "";
return std::string{R"inp(*Part, name=BeamPart return std::string{R"inp(*Part, name=BeamPart
*Node *Node
1, 0., 0., 0. 1, 0., 0., 0.
)inp"} + secondNode + R"inp(*Element, type=B33 )inp"} + second_node +
R"inp(*Element, type=B33
1, 1, 2 1, 1, 2
*Elset, elset=BeamSet *Elset, elset=BeamSet
1 1
@@ -145,17 +139,19 @@ S, SF
*Material, name=Steel *Material, name=Steel
*Elastic *Elastic
100., 0.25 100., 0.25
)inp" + boundaries + R"inp(*Step, name=Load, nlgeom=NO )inp" + boundaries +
R"inp(*Step, name=Load, nlgeom=NO
*Static *Static
0.1, 1., 0.01, 1. 0.1, 1., 0.01, 1.
*Cload *Cload
Tip, 1, 10. Tip, 1, 10.
)inp" + outputs + R"inp(*End Step )inp" + outputs +
R"inp(*End Step
)inp"; )inp";
} }
std::string allConstrainedShellDeck() { std::string AllConstrainedShellDeck() {
return R"inp(*Part, name=ShellPart return R"inp(*Part, name=ShellPart
*Node *Node
1, 0., 0., 0. 1, 0., 0., 0.
2, 1., 0., 0. 2, 1., 0., 0.
@@ -186,299 +182,277 @@ All, 1, 6
)inp"; )inp";
} }
Hdf5Handle openFile(const std::filesystem::path& path) { Hdf5Handle OpenFile(const std::filesystem::path& path) {
const hid_t file = H5Fopen( const hid_t file =
path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
if (file < 0) { if (file < 0) {
throw std::runtime_error{"Unable to open the CLI HDF5 artifact."}; throw std::runtime_error{"Unable to open the CLI HDF5 artifact."};
} }
return Hdf5Handle{file, H5Fclose}; return Hdf5Handle{file, H5Fclose};
} }
Hdf5Handle openDataset(const hid_t file, const std::string& path) { Hdf5Handle OpenDataset(const hid_t file, const std::string& path) {
const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT); const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT);
if (dataset < 0) { if (dataset < 0) {
throw std::runtime_error{"Unable to open mandatory dataset: " + path}; throw std::runtime_error{"Unable to open mandatory dataset: " + path};
} }
return Hdf5Handle{dataset, H5Dclose}; return Hdf5Handle{dataset, H5Dclose};
} }
std::vector<hsize_t> datasetDimensions( std::vector<hsize_t> DatasetDimensions(const hid_t file,
const hid_t file, const std::string& path) { const std::string& path) {
const auto dataset = openDataset(file, path); const auto dataset = OpenDataset(file, path);
Hdf5Handle space{H5Dget_space(dataset.get()), H5Sclose}; Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
const int rank = H5Sget_simple_extent_ndims(space.get()); const int rank = H5Sget_simple_extent_ndims(space.Get());
if (space.get() < 0 || rank < 0) { if (space.Get() < 0 || rank < 0) {
throw std::runtime_error{"Unable to inspect mandatory dataset dimensions."}; throw std::runtime_error{"Unable to inspect mandatory dataset dimensions."};
} }
std::vector<hsize_t> dimensions(static_cast<std::size_t>(rank)); std::vector<hsize_t> dimensions(static_cast<std::size_t>(rank));
if (rank > 0 && if (rank > 0 &&
H5Sget_simple_extent_dims(space.get(), dimensions.data(), nullptr) < 0) { H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr) < 0) {
throw std::runtime_error{"Unable to read mandatory dataset dimensions."}; throw std::runtime_error{"Unable to read mandatory dataset dimensions."};
} }
return dimensions; return dimensions;
} }
std::vector<double> readDoubleDataset( std::vector<double> ReadDoubleDataset(const hid_t file,
const hid_t file, const std::string& path) { const std::string& path) {
const auto dimensions = datasetDimensions(file, path); const auto dimensions = DatasetDimensions(file, path);
std::size_t count = 1U; std::size_t count = 1U;
for (const hsize_t dimension : dimensions) { for (const hsize_t dimension : dimensions) {
count *= static_cast<std::size_t>(dimension); count *= static_cast<std::size_t>(dimension);
} }
const auto dataset = openDataset(file, path); const auto dataset = OpenDataset(file, path);
std::vector<double> values(count); std::vector<double> values(count);
if (!values.empty() && if (!values.empty() && H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL,
H5Dread(dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()) < 0) {
H5P_DEFAULT, values.data()) < 0) { throw std::runtime_error{"Unable to read mandatory numeric results."};
throw std::runtime_error{"Unable to read mandatory numeric results."}; }
} return values;
return values;
} }
std::string readStringAttribute(const hid_t object, const char* name) { std::string ReadStringAttribute(const hid_t object, const char* name) {
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
Hdf5Handle type{H5Aget_type(attribute.get()), H5Tclose}; Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
if (attribute.get() < 0 || type.get() < 0 || if (attribute.Get() < 0 || type.Get() < 0 ||
H5Tget_class(type.get()) != H5T_STRING || H5Tget_class(type.Get()) != H5T_STRING ||
H5Tis_variable_str(type.get()) <= 0) { H5Tis_variable_str(type.Get()) <= 0) {
throw std::runtime_error{"Expected a variable-length string attribute."}; throw std::runtime_error{"Expected a variable-length string attribute."};
} }
char* raw = nullptr; char* raw = nullptr;
if (H5Aread(attribute.get(), type.get(), &raw) < 0 || raw == nullptr) { if (H5Aread(attribute.Get(), type.Get(), &raw) < 0 || raw == nullptr) {
throw std::runtime_error{"Unable to read the HDF5 identity attribute."}; throw std::runtime_error{"Unable to read the HDF5 identity attribute."};
} }
const std::string value{raw}; const std::string value{raw};
(void)H5free_memory(raw); (void)H5free_memory(raw);
return value; return value;
} }
void expectMandatoryInventory(const hid_t file) { void ExpectMandatoryInventory(const hid_t file) {
for (const char* path : { for (const char* path :
"/metadata", {"/metadata", "/model/nodes", "/model/elements",
"/model/nodes", "/steps/Step-1/frames/0/nodal/displacement",
"/model/elements", "/steps/Step-1/frames/0/nodal/reaction",
"/steps/Step-1/frames/0/nodal/displacement", "/steps/Step-1/frames/0/element/end_force_local",
"/steps/Step-1/frames/0/nodal/reaction", "/steps/Step-1/frames/0/element/section_resultant",
"/steps/Step-1/frames/0/element/end_force_local", "/steps/Step-1/frames/0/element/generalized_strain",
"/steps/Step-1/frames/0/element/section_resultant", "/steps/Step-1/frames/0/element/generalized_resultant",
"/steps/Step-1/frames/0/element/generalized_strain", "/steps/Step-1/frames/0/element/stress_s11", "/diagnostics"}) {
"/steps/Step-1/frames/0/element/generalized_resultant", EXPECT_GT(H5Lexists(file, path, H5P_DEFAULT), 0) << path;
"/steps/Step-1/frames/0/element/stress_s11", }
"/diagnostics"}) {
EXPECT_GT(H5Lexists(file, path, H5P_DEFAULT), 0) << path;
}
} }
void expectFesaHdf5Identity( void ExpectFesaHdf5Identity(const std::filesystem::path& output,
const std::filesystem::path& output, const std::filesystem::path& input) {
const std::filesystem::path& input) { ASSERT_TRUE(std::filesystem::exists(output));
ASSERT_TRUE(std::filesystem::exists(output)); ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0);
ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); const auto file = OpenFile(output);
const auto file = openFile(output); ExpectMandatoryInventory(file.Get());
expectMandatoryInventory(file.get()); Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
Hdf5Handle metadata{ ASSERT_GE(metadata.Get(), 0);
H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), H5Gclose}; EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"),
ASSERT_GE(metadata.get(), 0); "linear-static-3d-euler-beam");
EXPECT_EQ( const std::string normalized_input =
readStringAttribute(metadata.get(), "feature_id"), std::filesystem::absolute(input).lexically_normal().generic_u8string();
"linear-static-3d-euler-beam"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity")
const std::string normalizedInput = .find("path=" + normalized_input + ";content_identity="),
std::filesystem::absolute(input).lexically_normal().generic_u8string(); 0U);
EXPECT_EQ(
readStringAttribute(metadata.get(), "source_input_identity").find(
"path=" + normalizedInput + ";content_identity="),
0U);
} }
void expectShellHdf5Identity( void ExpectShellHdf5Identity(const std::filesystem::path& output,
const std::filesystem::path& output, const std::filesystem::path& input) {
const std::filesystem::path& input) { ASSERT_TRUE(std::filesystem::exists(output));
ASSERT_TRUE(std::filesystem::exists(output)); ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0);
ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); const auto file = OpenFile(output);
const auto file = openFile(output); Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
Hdf5Handle metadata{ ASSERT_GE(metadata.Get(), 0);
H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), H5Gclose}; EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"),
ASSERT_GE(metadata.get(), 0); "linear-static-mitc4-shell");
EXPECT_EQ( EXPECT_GT(H5Lexists(file.Get(),
readStringAttribute(metadata.get(), "feature_id"), "/steps/Step-1/frames/0/element/shell/generalized_strain",
"linear-static-mitc4-shell"); H5P_DEFAULT),
EXPECT_GT( 0);
H5Lexists( EXPECT_EQ(DatasetDimensions(file.Get(),
file.get(), "/steps/Step-1/frames/0/nodal/displacement"),
"/steps/Step-1/frames/0/element/shell/generalized_strain", (std::vector<hsize_t>{4U, 6U}));
H5P_DEFAULT), const std::string normalized_input =
0); std::filesystem::absolute(input).lexically_normal().generic_u8string();
EXPECT_EQ( EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity")
datasetDimensions( .find("path=" + normalized_input + ";content_identity="),
file.get(), 0U);
"/steps/Step-1/frames/0/nodal/displacement"),
(std::vector<hsize_t>{4U, 6U}));
const std::string normalizedInput =
std::filesystem::absolute(input).lexically_normal().generic_u8string();
EXPECT_EQ(
readStringAttribute(metadata.get(), "source_input_identity").find(
"path=" + normalizedInput + ";content_identity="),
0U);
} }
struct AppRun { struct AppRun {
int exitCode; int exit_code;
std::string standardError; std::string standard_error;
}; };
AppRun runApplication(const std::vector<std::string>& arguments) { AppRun RunApplication(const std::vector<std::string>& arguments) {
testing::internal::CaptureStderr(); testing::internal::CaptureStderr();
try { try {
const int exitCode = fesa::FesaApplication{}.run(arguments); const int exit_code = fesa::FesaApplication{}.Run(arguments);
return {exitCode, testing::internal::GetCapturedStderr()}; return {exit_code, testing::internal::GetCapturedStderr()};
} catch (...) { } catch (...) {
(void)testing::internal::GetCapturedStderr(); (void)testing::internal::GetCapturedStderr();
throw; throw;
} }
} }
void expectDiagnosticFieldOrder(const std::string& text) { void ExpectDiagnosticFieldOrder(const std::string& text) {
ASSERT_FALSE(text.empty()); ASSERT_FALSE(text.empty());
std::size_t cursor = 0U; std::size_t cursor = 0U;
for (const char* field : { for (const char* field : {"severity", "code", "file", "line", "keyword",
"severity", "code", "file", "line", "keyword", "entity_identity", "message"}) {
"entity_identity", "message"}) { const auto position = text.find(field, cursor);
const auto position = text.find(field, cursor); ASSERT_NE(position, std::string::npos)
ASSERT_NE(position, std::string::npos) << "Missing or out-of-order diagnostic field: " << field
<< "Missing or out-of-order diagnostic field: " << field << "\nstderr:\n"
<< "\nstderr:\n" << text; << text;
cursor = position + std::string{field}.size(); cursor = position + std::string{field}.size();
} }
} }
std::vector<std::string> explicitOutputArguments( std::vector<std::string> ExplicitOutputArguments(
const std::filesystem::path& input, const std::filesystem::path& input, const std::filesystem::path& output) {
const std::filesystem::path& output) { // FesaApplication receives argv[0]-excluded operands and options.
// FesaApplication receives argv[0]-excluded operands and options. return {input.string(), "--output", output.string()};
return {input.string(), "--output", output.string()};
} }
} // namespace } // namespace
TEST(LinearStaticCli, DefaultAndExplicitOutputProduceFesaHdf5) { TEST(LinearStaticCli, DefaultAndExplicitOutputProduceFesaHdf5) {
TempDirectory directory{"paths"}; TempDirectory directory{"paths"};
const auto input = directory.path() / "model.inp"; const auto input = directory.Path() / "model.inp";
writeText(input, axialDeck(true, false, false)); WriteText(input, AxialDeck(true, false, false));
const auto defaultOutput = directory.path() / "results.h5"; const auto default_output = directory.Path() / "results.h5";
{ {
CurrentDirectoryGuard currentDirectory{directory.path()}; CurrentDirectoryGuard current_directory{directory.Path()};
const auto result = runApplication({input.string()}); const auto result = RunApplication({input.string()});
ASSERT_EQ(result.exitCode, 0) << result.standardError; ASSERT_EQ(result.exit_code, 0) << result.standard_error;
} }
expectFesaHdf5Identity(defaultOutput, input); ExpectFesaHdf5Identity(default_output, input);
const auto explicitOutput = directory.path() / "named-output.h5"; const auto explicit_output = directory.Path() / "named-output.h5";
const auto result = runApplication( const auto result =
explicitOutputArguments(input, explicitOutput)); RunApplication(ExplicitOutputArguments(input, explicit_output));
ASSERT_EQ(result.exitCode, 0) << result.standardError; ASSERT_EQ(result.exit_code, 0) << result.standard_error;
expectFesaHdf5Identity(explicitOutput, input); ExpectFesaHdf5Identity(explicit_output, input);
} }
TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) { TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) {
TempDirectory directory{"exit-codes"}; TempDirectory directory{"exit-codes"};
const auto validInput = directory.path() / "valid.inp"; const auto valid_input = directory.Path() / "valid.inp";
const auto modelInput = directory.path() / "invalid-model.inp"; const auto model_input = directory.Path() / "invalid-model.inp";
const auto solverInput = directory.path() / "singular.inp"; const auto solver_input = directory.Path() / "singular.inp";
writeText(validInput, axialDeck(true, false, false)); WriteText(valid_input, AxialDeck(true, false, false));
writeText(modelInput, axialDeck(true, true, false)); WriteText(model_input, AxialDeck(true, true, false));
writeText(solverInput, axialDeck(false, false, false)); WriteText(solver_input, AxialDeck(false, false, false));
const auto success = runApplication(explicitOutputArguments( const auto success = RunApplication(
validInput, directory.path() / "success.h5")); ExplicitOutputArguments(valid_input, directory.Path() / "success.h5"));
const auto usage = runApplication({}); const auto usage = RunApplication({});
const auto missingInput = directory.path() / "missing.inp"; const auto missing_input = directory.Path() / "missing.inp";
const auto input = runApplication({missingInput.string()}); const auto input = RunApplication({missing_input.string()});
const auto repeatedOutput = runApplication( const auto repeated_output =
{missingInput.string(), "--output", "--output"}); RunApplication({missing_input.string(), "--output", "--output"});
const auto unknownOutputOption = runApplication( const auto unknown_output_option =
{missingInput.string(), "--output", "--bogus"}); RunApplication({missing_input.string(), "--output", "--bogus"});
const auto model = runApplication(explicitOutputArguments( const auto model = RunApplication(ExplicitOutputArguments(
modelInput, directory.path() / "model-failure.h5")); model_input, directory.Path() / "model-failure.h5"));
const auto solver = runApplication(explicitOutputArguments( const auto solver = RunApplication(ExplicitOutputArguments(
solverInput, directory.path() / "solver-failure.h5")); solver_input, directory.Path() / "solver-failure.h5"));
const auto output = runApplication(explicitOutputArguments( const auto output = RunApplication(ExplicitOutputArguments(
validInput, valid_input, directory.Path() / "nonexistent-parent" / "results.h5"));
directory.path() / "nonexistent-parent" / "results.h5"));
EXPECT_EQ(success.exitCode, 0) << success.standardError; EXPECT_EQ(success.exit_code, 0) << success.standard_error;
EXPECT_EQ(usage.exitCode, 2); EXPECT_EQ(usage.exit_code, 2);
EXPECT_EQ(input.exitCode, 3); EXPECT_EQ(input.exit_code, 3);
EXPECT_EQ(model.exitCode, 4); EXPECT_EQ(model.exit_code, 4);
EXPECT_EQ(solver.exitCode, 5); EXPECT_EQ(solver.exit_code, 5);
EXPECT_EQ(output.exitCode, 6); EXPECT_EQ(output.exit_code, 6);
expectDiagnosticFieldOrder(usage.standardError); ExpectDiagnosticFieldOrder(usage.standard_error);
expectDiagnosticFieldOrder(input.standardError); ExpectDiagnosticFieldOrder(input.standard_error);
expectDiagnosticFieldOrder(model.standardError); ExpectDiagnosticFieldOrder(model.standard_error);
expectDiagnosticFieldOrder(solver.standardError); ExpectDiagnosticFieldOrder(solver.standard_error);
expectDiagnosticFieldOrder(output.standardError); ExpectDiagnosticFieldOrder(output.standard_error);
EXPECT_EQ(repeatedOutput.exitCode, 2); EXPECT_EQ(repeated_output.exit_code, 2);
expectDiagnosticFieldOrder(repeatedOutput.standardError); ExpectDiagnosticFieldOrder(repeated_output.standard_error);
EXPECT_NE(repeatedOutput.standardError.find("code=cli-usage"), EXPECT_NE(repeated_output.standard_error.find("code=cli-usage"),
std::string::npos); std::string::npos);
EXPECT_EQ(unknownOutputOption.exitCode, 2); EXPECT_EQ(unknown_output_option.exit_code, 2);
expectDiagnosticFieldOrder(unknownOutputOption.standardError); ExpectDiagnosticFieldOrder(unknown_output_option.standard_error);
EXPECT_NE(unknownOutputOption.standardError.find("code=cli-usage"), EXPECT_NE(unknown_output_option.standard_error.find("code=cli-usage"),
std::string::npos); std::string::npos);
} }
TEST(LinearStaticCli, OutputRequestsDoNotFilterMandatoryResults) { TEST(LinearStaticCli, OutputRequestsDoNotFilterMandatoryResults) {
TempDirectory directory{"output-requests"}; TempDirectory directory{"output-requests"};
const auto plainInput = directory.path() / "plain.inp"; const auto plain_input = directory.Path() / "plain.inp";
const auto requestedInput = directory.path() / "requested.inp"; const auto requested_input = directory.Path() / "requested.inp";
const auto plainOutput = directory.path() / "plain.h5"; const auto plain_output = directory.Path() / "plain.h5";
const auto requestedOutput = directory.path() / "requested.h5"; const auto requested_output = directory.Path() / "requested.h5";
writeText(plainInput, axialDeck(true, false, false)); WriteText(plain_input, AxialDeck(true, false, false));
writeText(requestedInput, axialDeck(true, false, true)); WriteText(requested_input, AxialDeck(true, false, true));
const auto plain = runApplication( const auto plain =
explicitOutputArguments(plainInput, plainOutput)); RunApplication(ExplicitOutputArguments(plain_input, plain_output));
const auto requested = runApplication( const auto requested = RunApplication(
explicitOutputArguments(requestedInput, requestedOutput)); ExplicitOutputArguments(requested_input, requested_output));
ASSERT_EQ(plain.exitCode, 0) << plain.standardError; ASSERT_EQ(plain.exit_code, 0) << plain.standard_error;
ASSERT_EQ(requested.exitCode, 0) << requested.standardError; ASSERT_EQ(requested.exit_code, 0) << requested.standard_error;
const auto plainFile = openFile(plainOutput); const auto plain_file = OpenFile(plain_output);
const auto requestedFile = openFile(requestedOutput); const auto requested_file = OpenFile(requested_output);
expectMandatoryInventory(plainFile.get()); ExpectMandatoryInventory(plain_file.Get());
expectMandatoryInventory(requestedFile.get()); ExpectMandatoryInventory(requested_file.Get());
for (const char* suffix : { for (const char* suffix :
"/nodal/displacement", {"/nodal/displacement", "/nodal/reaction", "/element/end_force_local",
"/nodal/reaction", "/element/section_resultant", "/element/generalized_strain",
"/element/end_force_local", "/element/generalized_resultant"}) {
"/element/section_resultant", const std::string path = std::string{kStepRoot} + suffix;
"/element/generalized_strain", EXPECT_EQ(ReadDoubleDataset(requested_file.Get(), path),
"/element/generalized_resultant"}) { ReadDoubleDataset(plain_file.Get(), path))
const std::string path = std::string{kStepRoot} + suffix; << path;
EXPECT_EQ( }
readDoubleDataset(requestedFile.get(), path), EXPECT_EQ(DatasetDimensions(plain_file.Get(), "/diagnostics"),
readDoubleDataset(plainFile.get(), path)) std::vector<hsize_t>({0U}));
<< path; const auto requested_diagnostics =
} DatasetDimensions(requested_file.Get(), "/diagnostics");
EXPECT_EQ(datasetDimensions(plainFile.get(), "/diagnostics"), ASSERT_EQ(requested_diagnostics.size(), 1U);
std::vector<hsize_t>({0U})); EXPECT_GT(requested_diagnostics[0U], 0U);
const auto requestedDiagnostics =
datasetDimensions(requestedFile.get(), "/diagnostics");
ASSERT_EQ(requestedDiagnostics.size(), 1U);
EXPECT_GT(requestedDiagnostics[0U], 0U);
} }
// MITC4-FLOW-001: shell input uses the unchanged application route and syntax. // MITC4-FLOW-001: shell input uses the unchanged application route and syntax.
TEST(Mitc4ShellCli, WritesShellHdf5ThroughTheExistingApplicationRoute) { TEST(Mitc4ShellCli, WritesShellHdf5ThroughTheExistingApplicationRoute) {
TempDirectory directory{"shell-route"}; TempDirectory directory{"shell-route"};
const auto input = directory.path() / "shell.inp"; const auto input = directory.Path() / "shell.inp";
const auto output = directory.path() / "shell-results.h5"; const auto output = directory.Path() / "shell-results.h5";
writeText(input, allConstrainedShellDeck()); WriteText(input, AllConstrainedShellDeck());
const auto result = runApplication(explicitOutputArguments(input, output)); const auto result = RunApplication(ExplicitOutputArguments(input, output));
ASSERT_EQ(result.exitCode, 0) << result.standardError; ASSERT_EQ(result.exit_code, 0) << result.standard_error;
expectShellHdf5Identity(output, input); ExpectShellHdf5Identity(output, input);
} }
+189 -217
View File
@@ -1,10 +1,5 @@
#include "reference_comparison.hpp"
#include "fesa/app/fesa_application.hpp"
#include <hdf5.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <hdf5.h>
#include <algorithm> #include <algorithm>
#include <array> #include <array>
@@ -17,6 +12,9 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "fesa/app/fesa_application.h"
#include "reference_comparison.h"
#ifndef FESA_TEST_SOURCE_DIR #ifndef FESA_TEST_SOURCE_DIR
#error FESA_TEST_SOURCE_DIR must identify the repository root. #error FESA_TEST_SOURCE_DIR must identify the repository root.
#endif #endif
@@ -27,250 +25,224 @@
namespace { namespace {
constexpr const char* kStressPath = constexpr const char* kStressPath = "/steps/Step-1/frames/0/element/stress_s11";
"/steps/Step-1/frames/0/element/stress_s11";
constexpr std::size_t kExpectedRowCount = 176U; constexpr std::size_t kExpectedRowCount = 176U;
constexpr std::size_t kExpectedMetricCount = 16U; constexpr std::size_t kExpectedMetricCount = 16U;
class Hdf5Handle { class Hdf5Handle {
public: public:
using Closer = herr_t (*)(hid_t); using Closer = herr_t (*)(hid_t);
Hdf5Handle(const hid_t value, Closer closer) Hdf5Handle(const hid_t value, Closer closer)
: value_{value}, closer_{closer} {} : value_{value}, closer_{closer} {}
Hdf5Handle(const Hdf5Handle&) = delete; Hdf5Handle(const Hdf5Handle&) = delete;
Hdf5Handle& operator=(const Hdf5Handle&) = delete; Hdf5Handle& operator=(const Hdf5Handle&) = delete;
~Hdf5Handle() { ~Hdf5Handle() {
if (value_ >= 0 && closer_ != nullptr) { if (value_ >= 0 && closer_ != nullptr) {
(void)closer_(value_); (void)closer_(value_);
}
} }
}
hid_t get() const noexcept { return value_; } hid_t Get() const noexcept { return value_; }
private: private:
hid_t value_; hid_t value_;
Closer closer_; Closer closer_;
}; };
struct ReferenceSnapshotEntry { struct ReferenceSnapshotEntry {
std::filesystem::path relativePath; std::filesystem::path relative_path;
bool isDirectory; bool is_directory;
std::string bytes; std::string bytes;
std::filesystem::file_time_type lastWriteTime; std::filesystem::file_time_type last_write_time;
}; };
std::string readBytes(const std::filesystem::path& path) { std::string ReadBytes(const std::filesystem::path& path) {
std::ifstream stream{path, std::ios::binary}; std::ifstream stream{path, std::ios::binary};
if (!stream) { if (!stream) {
throw std::runtime_error{"Unable to read reference evidence: " + throw std::runtime_error{"Unable to read reference evidence: " +
path.string()}; path.string()};
} }
return {std::istreambuf_iterator<char>{stream}, return {std::istreambuf_iterator<char>{stream},
std::istreambuf_iterator<char>{}}; std::istreambuf_iterator<char>{}};
} }
std::vector<ReferenceSnapshotEntry> snapshotTree( std::vector<ReferenceSnapshotEntry> SnapshotTree(
const std::filesystem::path& root) { const std::filesystem::path& root) {
std::vector<ReferenceSnapshotEntry> entries; std::vector<ReferenceSnapshotEntry> entries;
for (const auto& entry : std::filesystem::recursive_directory_iterator{root}) { for (const auto& entry :
const bool isDirectory = entry.is_directory(); std::filesystem::recursive_directory_iterator{root}) {
if (!isDirectory && !entry.is_regular_file()) { const bool is_directory = entry.is_directory();
throw std::runtime_error{"Unexpected reference-tree entry type."}; if (!is_directory && !entry.is_regular_file()) {
} throw std::runtime_error{"Unexpected reference-tree entry type."};
entries.push_back({
std::filesystem::relative(entry.path(), root),
isDirectory,
isDirectory ? std::string{} : readBytes(entry.path()),
entry.last_write_time()});
} }
std::sort( entries.push_back({std::filesystem::relative(entry.path(), root),
entries.begin(), is_directory,
entries.end(), is_directory ? std::string{} : ReadBytes(entry.path()),
[](const ReferenceSnapshotEntry& left, entry.last_write_time()});
const ReferenceSnapshotEntry& right) { }
return left.relativePath.generic_string() < std::sort(entries.begin(), entries.end(),
right.relativePath.generic_string(); [](const ReferenceSnapshotEntry& left,
}); const ReferenceSnapshotEntry& right) {
return entries; return left.relative_path.generic_string() <
right.relative_path.generic_string();
});
return entries;
} }
void expectTreeUnchanged( void ExpectTreeUnchanged(const std::vector<ReferenceSnapshotEntry>& before,
const std::vector<ReferenceSnapshotEntry>& before, const std::vector<ReferenceSnapshotEntry>& after) {
const std::vector<ReferenceSnapshotEntry>& after) { ASSERT_EQ(after.size(), before.size());
ASSERT_EQ(after.size(), before.size()); for (std::size_t index = 0U; index < before.size(); ++index) {
for (std::size_t index = 0U; index < before.size(); ++index) { EXPECT_EQ(after[index].relative_path, before[index].relative_path);
EXPECT_EQ(after[index].relativePath, before[index].relativePath); EXPECT_EQ(after[index].is_directory, before[index].is_directory);
EXPECT_EQ(after[index].isDirectory, before[index].isDirectory); EXPECT_EQ(after[index].bytes, before[index].bytes)
EXPECT_EQ(after[index].bytes, before[index].bytes) << before[index].relative_path.string();
<< before[index].relativePath.string(); EXPECT_EQ(after[index].last_write_time, before[index].last_write_time)
EXPECT_EQ(after[index].lastWriteTime, before[index].lastWriteTime) << before[index].relative_path.string();
<< before[index].relativePath.string(); }
}
} }
double norm(const std::array<double, 3>& value) { double Norm(const std::array<double, 3>& value) {
return std::sqrt( return std::sqrt(value[0U] * value[0U] + value[1U] * value[1U] +
value[0U] * value[0U] + value[1U] * value[1U] + value[2U] * value[2U]);
value[2U] * value[2U]);
} }
std::array<double, 3> sum( std::array<double, 3> Sum(const std::array<double, 3>& left,
const std::array<double, 3>& left, const std::array<double, 3>& right) {
const std::array<double, 3>& right) { return {left[0U] + right[0U], left[1U] + right[1U], left[2U] + right[2U]};
return {
left[0U] + right[0U],
left[1U] + right[1U],
left[2U] + right[2U]};
} }
std::size_t stressRowCount(const std::filesystem::path& results) { std::size_t StressRowCount(const std::filesystem::path& results) {
const hid_t fileId = const hid_t file_id =
H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
if (fileId < 0) { if (file_id < 0) {
throw std::runtime_error{"Unable to open authoritative HDF5 output."}; throw std::runtime_error{"Unable to open authoritative HDF5 output."};
} }
const Hdf5Handle file{fileId, H5Fclose}; const Hdf5Handle file{file_id, H5Fclose};
if (H5Lexists(file.get(), kStressPath, H5P_DEFAULT) <= 0) { if (H5Lexists(file.Get(), kStressPath, H5P_DEFAULT) <= 0) {
throw std::runtime_error{"Mandatory stress_s11 dataset is missing."}; throw std::runtime_error{"Mandatory stress_s11 dataset is missing."};
} }
const hid_t datasetId = H5Dopen2(file.get(), kStressPath, H5P_DEFAULT); const hid_t dataset_id = H5Dopen2(file.Get(), kStressPath, H5P_DEFAULT);
if (datasetId < 0) { if (dataset_id < 0) {
throw std::runtime_error{"Unable to open mandatory stress_s11 dataset."}; throw std::runtime_error{"Unable to open mandatory stress_s11 dataset."};
} }
const Hdf5Handle dataset{datasetId, H5Dclose}; const Hdf5Handle dataset{dataset_id, H5Dclose};
const hid_t spaceId = H5Dget_space(dataset.get()); const hid_t space_id = H5Dget_space(dataset.Get());
if (spaceId < 0) { if (space_id < 0) {
throw std::runtime_error{"Unable to inspect stress_s11 dataspace."}; throw std::runtime_error{"Unable to inspect stress_s11 dataspace."};
} }
const Hdf5Handle space{spaceId, H5Sclose}; const Hdf5Handle space{space_id, H5Sclose};
if (H5Sget_simple_extent_ndims(space.get()) != 1) { if (H5Sget_simple_extent_ndims(space.Get()) != 1) {
throw std::runtime_error{"stress_s11 must be a flat row dataset."}; throw std::runtime_error{"stress_s11 must be a flat row dataset."};
} }
hsize_t count = 0U; hsize_t count = 0U;
if (H5Sget_simple_extent_dims(space.get(), &count, nullptr) < 0) { if (H5Sget_simple_extent_dims(space.Get(), &count, nullptr) < 0) {
throw std::runtime_error{"Unable to read stress_s11 extent."}; throw std::runtime_error{"Unable to read stress_s11 extent."};
} }
return static_cast<std::size_t>(count); return static_cast<std::size_t>(count);
} }
} // namespace } // namespace
TEST(B33ReferenceComparison, TEST(B33ReferenceComparison, GeneratesAuthoritativeHdf5AndComparisonEvidence) {
GeneratesAuthoritativeHdf5AndComparisonEvidence) { const std::filesystem::path source_root{FESA_TEST_SOURCE_DIR};
const std::filesystem::path sourceRoot{FESA_TEST_SOURCE_DIR}; const std::filesystem::path binary_root{FESA_TEST_BINARY_DIR};
const std::filesystem::path binaryRoot{FESA_TEST_BINARY_DIR}; const auto reference_directory =
const auto referenceDirectory = source_root / "reference" / "cantilever beam";
sourceRoot / "reference" / "cantilever beam"; const auto input = reference_directory / "cantilever beam.inp";
const auto input = referenceDirectory / "cantilever beam.inp"; const auto output_directory =
const auto outputDirectory = binary_root / "reference" / "cantilever-beam-b33";
binaryRoot / "reference" / "cantilever-beam-b33"; const auto results = output_directory / "results.h5";
const auto results = outputDirectory / "results.h5"; const auto comparison = output_directory / "comparison.json";
const auto comparison = outputDirectory / "comparison.json";
// Only the exact build-local evidence directory is reset; the approved // Only the exact build-local evidence directory is reset; the approved
// reference tree is snapshotted and subsequently opened read-only. // reference tree is snapshotted and subsequently opened read-only.
std::error_code error; std::error_code error;
std::filesystem::remove_all(outputDirectory, error); std::filesystem::remove_all(output_directory, error);
error.clear(); error.clear();
ASSERT_TRUE(std::filesystem::create_directories(outputDirectory, error)); ASSERT_TRUE(std::filesystem::create_directories(output_directory, error));
ASSERT_FALSE(error); ASSERT_FALSE(error);
const auto referenceBefore = snapshotTree(referenceDirectory); const auto reference_before = SnapshotTree(reference_directory);
ASSERT_EQ(referenceBefore.size(), 4U); ASSERT_EQ(reference_before.size(), 4U);
EXPECT_EQ( EXPECT_EQ(reference_before[0U].relative_path,
referenceBefore[0U].relativePath, std::filesystem::path{"cantilever beam displacements.csv"});
std::filesystem::path{"cantilever beam displacements.csv"}); EXPECT_EQ(reference_before[1U].relative_path,
EXPECT_EQ( std::filesystem::path{"cantilever beam elemental forces.csv"});
referenceBefore[1U].relativePath, EXPECT_EQ(reference_before[2U].relative_path,
std::filesystem::path{"cantilever beam elemental forces.csv"}); std::filesystem::path{"cantilever beam reactions.csv"});
EXPECT_EQ( EXPECT_EQ(reference_before[3U].relative_path,
referenceBefore[2U].relativePath, std::filesystem::path{"cantilever beam.inp"});
std::filesystem::path{"cantilever beam reactions.csv"});
EXPECT_EQ(
referenceBefore[3U].relativePath,
std::filesystem::path{"cantilever beam.inp"});
fesa::FesaApplication application; fesa::FesaApplication application;
// FesaApplication receives application operands/options; main strips argv[0]. // FesaApplication receives application operands/options; main strips argv[0].
ASSERT_EQ( ASSERT_EQ(application.Run({input.string(), "--output", results.string()}), 0);
application.run( ASSERT_TRUE(std::filesystem::is_regular_file(results));
{input.string(), "--output", results.string()}), ASSERT_GT(H5Fis_hdf5(results.string().c_str()), 0);
0); EXPECT_GT(StressRowCount(results), 0U);
ASSERT_TRUE(std::filesystem::is_regular_file(results));
ASSERT_GT(H5Fis_hdf5(results.string().c_str()), 0);
EXPECT_GT(stressRowCount(results), 0U);
auto comparisonResult = fesa::test::ReferenceComparison::compare( auto comparison_result =
results, referenceDirectory); fesa::test::ReferenceComparison::Compare(results, reference_directory);
ASSERT_TRUE(comparisonResult.HasValue()); ASSERT_TRUE(comparison_result.HasValue());
const auto& report = comparisonResult.Value(); const auto& report = comparison_result.Value();
ASSERT_TRUE(report.passed); ASSERT_TRUE(report.passed);
ASSERT_EQ(report.rows.size(), kExpectedRowCount); ASSERT_EQ(report.rows.size(), kExpectedRowCount);
ASSERT_EQ(report.metrics.size(), kExpectedMetricCount); ASSERT_EQ(report.metrics.size(), kExpectedMetricCount);
EXPECT_TRUE(std::all_of( EXPECT_TRUE(std::all_of(
report.rows.begin(), report.rows.begin(), report.rows.end(),
report.rows.end(), [](const fesa::test::RowDecision& row) {
[](const fesa::test::RowDecision& row) { return row.passed && std::isfinite(row.absolute_error) &&
return row.passed && std::isfinite(row.absoluteError) && std::isfinite(row.tolerance) && row.tolerance > 0.0 &&
std::isfinite(row.tolerance) && row.tolerance > 0.0 && row.fesa.model_id == "cantilever-beam-b33" &&
row.fesa.modelId == "cantilever-beam-b33" && row.reference.model_id == "cantilever-beam-b33" &&
row.reference.modelId == "cantilever-beam-b33" && row.fesa.step_name == "Step-1" &&
row.fesa.stepName == "Step-1" && row.reference.step_name == "Step-1" &&
row.reference.stepName == "Step-1" && row.fesa.frame_index == 0U && row.reference.frame_index == 0U &&
row.fesa.frameIndex == 0U && row.fesa.instance_name == "PART-1_1-1" &&
row.reference.frameIndex == 0U && row.reference.instance_name == "PART-1_1-1" &&
row.fesa.instanceName == "PART-1_1-1" && !row.fesa.hdf5_dataset_path.empty();
row.reference.instanceName == "PART-1_1-1" && }));
!row.fesa.hdf5DatasetPath.empty(); EXPECT_TRUE(
})); std::all_of(report.metrics.begin(), report.metrics.end(),
EXPECT_TRUE(std::all_of( [](const fesa::test::ComponentMetrics& metric) {
report.metrics.begin(), return std::isfinite(metric.reference_scale) &&
report.metrics.end(), std::isfinite(metric.maximum_absolute_error) &&
[](const fesa::test::ComponentMetrics& metric) { std::isfinite(metric.maximum_normalized_error) &&
return std::isfinite(metric.referenceScale) && std::isfinite(metric.rms_error) &&
std::isfinite(metric.maximumAbsoluteError) && std::isfinite(metric.norm_error) &&
std::isfinite(metric.maximumNormalizedError) && metric.maximum_normalized_error <= 1.0;
std::isfinite(metric.rmsError) && }));
std::isfinite(metric.normError) &&
metric.maximumNormalizedError <= 1.0;
}));
EXPECT_FALSE(report.stressComparisonApplicable); EXPECT_FALSE(report.stress_comparison_applicable);
EXPECT_NE(report.stressComparisonReason.find("N/A"), std::string::npos); EXPECT_NE(report.stress_comparison_reason.find("N/A"), std::string::npos);
EXPECT_NE(report.stressComparisonReason.find("HDF5"), std::string::npos); EXPECT_NE(report.stress_comparison_reason.find("HDF5"), std::string::npos);
EXPECT_TRUE(report.physicsEvidence.endpointConsistencyPassed); EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed);
EXPECT_TRUE(std::isfinite(report.physicsEvidence.freeResidualNorm)); EXPECT_TRUE(std::isfinite(report.physics_evidence.free_residual_norm));
EXPECT_LE(report.physicsEvidence.freeResidualNorm, 1.0e-3); EXPECT_LE(report.physics_evidence.free_residual_norm, 1.0e-3);
EXPECT_LE( EXPECT_LE(Norm(Sum(report.physics_evidence.applied_force,
norm(sum( report.physics_evidence.reaction_force)),
report.physicsEvidence.appliedForce, 1.0e-3);
report.physicsEvidence.reactionForce)), EXPECT_LE(Norm(Sum(report.physics_evidence.applied_moment_about_origin,
1.0e-3); report.physics_evidence.reaction_moment_about_origin)),
EXPECT_LE( 1.0e-2);
norm(sum(
report.physicsEvidence.appliedMomentAboutOrigin,
report.physicsEvidence.reactionMomentAboutOrigin)),
1.0e-2);
ASSERT_TRUE( ASSERT_TRUE(fesa::test::ReferenceComparison::WriteDeterministicJson(
fesa::test::ReferenceComparison::writeDeterministicJson( report, comparison)
report, comparison) .IsOk());
.IsOk()); ASSERT_TRUE(std::filesystem::is_regular_file(comparison));
ASSERT_TRUE(std::filesystem::is_regular_file(comparison)); const std::string json = ReadBytes(comparison);
const std::string json = readBytes(comparison); EXPECT_NE(json.find("\"stress_comparison_applicable\":false"),
EXPECT_NE(json.find("\"stress_comparison_applicable\":false"), std::string::npos);
std::string::npos); EXPECT_NE(json.find("\"physics_evidence\""), std::string::npos);
EXPECT_NE(json.find("\"physics_evidence\""), std::string::npos);
std::vector<std::string> generatedNames; std::vector<std::string> generated_names;
for (const auto& entry : for (const auto& entry :
std::filesystem::directory_iterator{outputDirectory}) { std::filesystem::directory_iterator{output_directory}) {
generatedNames.push_back(entry.path().filename().string()); generated_names.push_back(entry.path().filename().string());
} }
std::sort(generatedNames.begin(), generatedNames.end()); std::sort(generated_names.begin(), generated_names.end());
EXPECT_EQ( EXPECT_EQ(generated_names,
generatedNames, (std::vector<std::string>{"comparison.json", "results.h5"}));
(std::vector<std::string>{"comparison.json", "results.h5"}));
expectTreeUnchanged(referenceBefore, snapshotTree(referenceDirectory)); ExpectTreeUnchanged(reference_before, SnapshotTree(reference_directory));
} }
+110 -122
View File
@@ -1,7 +1,3 @@
#include "mitc4_reference_comparison.hpp"
#include "fesa/app/fesa_application.hpp"
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <algorithm> #include <algorithm>
@@ -14,6 +10,9 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "fesa/app/fesa_application.h"
#include "mitc4_reference_comparison.h"
#ifndef FESA_TEST_SOURCE_DIR #ifndef FESA_TEST_SOURCE_DIR
#error FESA_TEST_SOURCE_DIR must identify the repository root. #error FESA_TEST_SOURCE_DIR must identify the repository root.
#endif #endif
@@ -30,150 +29,139 @@ constexpr const char* kIntegrationRule =
constexpr std::size_t kNodeCount = 49U; constexpr std::size_t kNodeCount = 49U;
constexpr std::size_t kComponentCount = 6U; constexpr std::size_t kComponentCount = 6U;
std::string readBytes(const std::filesystem::path& path) { std::string ReadBytes(const std::filesystem::path& path) {
std::ifstream stream{path, std::ios::binary}; std::ifstream stream{path, std::ios::binary};
if (!stream) { if (!stream) {
throw std::runtime_error{"Unable to read declared reference artifact."}; throw std::runtime_error{"Unable to read declared reference artifact."};
} }
return {std::istreambuf_iterator<char>{stream}, return {std::istreambuf_iterator<char>{stream},
std::istreambuf_iterator<char>{}}; std::istreambuf_iterator<char>{}};
} }
struct ArtifactSnapshot { struct ArtifactSnapshot {
std::string bytes; std::string bytes;
std::filesystem::file_time_type lastWriteTime; std::filesystem::file_time_type last_write_time;
}; };
ArtifactSnapshot snapshot(const std::filesystem::path& path) { ArtifactSnapshot Snapshot(const std::filesystem::path& path) {
return {readBytes(path), std::filesystem::last_write_time(path)}; return {ReadBytes(path), std::filesystem::last_write_time(path)};
} }
void expectUnchanged( void ExpectUnchanged(const std::filesystem::path& path,
const std::filesystem::path& path, const ArtifactSnapshot& before) { const ArtifactSnapshot& before) {
EXPECT_EQ(readBytes(path), before.bytes) << path.string(); EXPECT_EQ(ReadBytes(path), before.bytes) << path.string();
EXPECT_EQ(std::filesystem::last_write_time(path), before.lastWriteTime) EXPECT_EQ(std::filesystem::last_write_time(path), before.last_write_time)
<< path.string(); << path.string();
} }
struct CaseEvidence { struct CaseEvidence {
fesa::test::Mitc4ComparisonReport report; fesa::test::Mitc4ComparisonReport report;
std::filesystem::path comparisonJson; std::filesystem::path comparison_json;
}; };
CaseEvidence runCase( CaseEvidence RunCase(const std::string& case_id,
const std::string& caseId, const std::string& source_element_type,
const std::string& sourceElementType, const std::filesystem::path& reference_directory,
const std::filesystem::path& referenceDirectory, const std::filesystem::path& input,
const std::filesystem::path& input, const std::filesystem::path& csv,
const std::filesystem::path& csv, const std::string& output_name) {
const std::string& outputName) { const auto input_before = Snapshot(input);
const auto inputBefore = snapshot(input); const auto csv_before = Snapshot(csv);
const auto csvBefore = snapshot(csv); const std::filesystem::path output_directory =
const std::filesystem::path outputDirectory = std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / output_name;
std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / outputName; std::error_code error;
std::error_code error; std::filesystem::remove_all(output_directory, error);
std::filesystem::remove_all(outputDirectory, error); error.clear();
error.clear(); if (!std::filesystem::create_directories(output_directory, error) || error) {
if (!std::filesystem::create_directories(outputDirectory, error) || error) { throw std::runtime_error{"Unable to create MITC4 evidence directory."};
throw std::runtime_error{"Unable to create MITC4 evidence directory."}; }
} const auto results = output_directory / "results.h5";
const auto results = outputDirectory / "results.h5"; const auto comparison = output_directory / "comparison.json";
const auto comparison = outputDirectory / "comparison.json";
fesa::FesaApplication application; fesa::FesaApplication application;
EXPECT_EQ( EXPECT_EQ(application.Run({input.string(), "--output", results.string()}), 0);
application.run({input.string(), "--output", results.string()}), 0); EXPECT_TRUE(std::filesystem::is_regular_file(results));
EXPECT_TRUE(std::filesystem::is_regular_file(results)); auto comparison_result = fesa::test::Mitc4ReferenceComparison::Compare(
auto comparisonResult = fesa::test::Mitc4ReferenceComparison::compare( {case_id, source_element_type, input, csv, results});
{caseId, sourceElementType, input, csv, results}); if (!comparison_result.HasValue()) {
if (!comparisonResult.HasValue()) { std::string diagnostics;
std::string diagnostics; for (const auto& diagnostic : comparison_result.GetStatus().Diagnostics()) {
for (const auto& diagnostic : diagnostics += "\n" + diagnostic.code + ": " + diagnostic.message;
comparisonResult.GetStatus().Diagnostics()) {
diagnostics += "\n" + diagnostic.code + ": " + diagnostic.message;
}
ADD_FAILURE() << "MITC4 comparison precheck failed for " << caseId
<< diagnostics;
return {{}, comparison};
} }
EXPECT_TRUE( ADD_FAILURE() << "MITC4 comparison precheck failed for " << case_id
fesa::test::Mitc4ReferenceComparison::writeDeterministicJson( << diagnostics;
comparisonResult.Value(), comparison) return {{}, comparison};
.IsOk()); }
EXPECT_TRUE(std::filesystem::is_regular_file(comparison)); EXPECT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson(
comparison_result.Value(), comparison)
.IsOk());
EXPECT_TRUE(std::filesystem::is_regular_file(comparison));
std::vector<std::string> generated; std::vector<std::string> generated;
for (const auto& entry : for (const auto& entry :
std::filesystem::directory_iterator{outputDirectory}) { std::filesystem::directory_iterator{output_directory}) {
generated.push_back(entry.path().filename().string()); generated.push_back(entry.path().filename().string());
} }
std::sort(generated.begin(), generated.end()); std::sort(generated.begin(), generated.end());
EXPECT_EQ( EXPECT_EQ(generated,
generated, (std::vector<std::string>{"comparison.json", "results.h5"}));
(std::vector<std::string>{"comparison.json", "results.h5"})); EXPECT_TRUE(std::filesystem::is_directory(reference_directory));
EXPECT_TRUE(std::filesystem::is_directory(referenceDirectory)); ExpectUnchanged(input, input_before);
expectUnchanged(input, inputBefore); ExpectUnchanged(csv, csv_before);
expectUnchanged(csv, csvBefore); return {std::move(comparison_result.Value()), comparison};
return {std::move(comparisonResult.Value()), comparison};
} }
void expectCommonMetadata( void ExpectCommonMetadata(const fesa::test::Mitc4ComparisonReport& report,
const fesa::test::Mitc4ComparisonReport& report, const std::string& case_id,
const std::string& caseId, const std::string& source_element_type) {
const std::string& sourceElementType) { EXPECT_EQ(report.case_id, case_id);
EXPECT_EQ(report.caseId, caseId); EXPECT_EQ(report.source_element_type, source_element_type);
EXPECT_EQ(report.sourceElementType, sourceElementType); EXPECT_EQ(report.internal_formulation, kInternalFormulation);
EXPECT_EQ(report.internalFormulation, kInternalFormulation); EXPECT_EQ(report.integration_rule, kIntegrationRule);
EXPECT_EQ(report.integrationRule, kIntegrationRule);
} }
void expectComparisonCoverage( void ExpectComparisonCoverage(const fesa::test::Mitc4ComparisonReport& report) {
const fesa::test::Mitc4ComparisonReport& report) { ASSERT_EQ(report.rows.size(), kNodeCount * kComponentCount);
ASSERT_EQ(report.rows.size(), kNodeCount * kComponentCount); ASSERT_EQ(report.metrics.size(), kComponentCount);
ASSERT_EQ(report.metrics.size(), kComponentCount); ASSERT_EQ(report.vector_metrics.size(), kNodeCount);
ASSERT_EQ(report.vectorMetrics.size(), kNodeCount); EXPECT_TRUE(report.passed);
EXPECT_TRUE(report.passed); const std::size_t blocking_rows = static_cast<std::size_t>(std::count_if(
const std::size_t blockingRows = static_cast<std::size_t>(std::count_if( report.rows.begin(), report.rows.end(),
report.rows.begin(), report.rows.end(), [](const fesa::test::Mitc4RowDecision& row) { return row.blocking; }));
[](const fesa::test::Mitc4RowDecision& row) { const std::size_t rotation_rows = report.rows.size() - blocking_rows;
return row.blocking; EXPECT_EQ(blocking_rows, kNodeCount * 3U);
})); EXPECT_EQ(rotation_rows, kNodeCount * 3U);
const std::size_t rotationRows = report.rows.size() - blockingRows; EXPECT_TRUE(std::all_of(report.rows.begin(), report.rows.end(),
EXPECT_EQ(blockingRows, kNodeCount * 3U); [](const fesa::test::Mitc4RowDecision& row) {
EXPECT_EQ(rotationRows, kNodeCount * 3U); return !row.blocking || row.within_tolerance;
EXPECT_TRUE(std::all_of( }));
report.rows.begin(), report.rows.end(), EXPECT_EQ(report.warnings.size(),
[](const fesa::test::Mitc4RowDecision& row) { static_cast<std::size_t>(
return !row.blocking || row.withinTolerance; std::count_if(report.rows.begin(), report.rows.end(),
})); [](const fesa::test::Mitc4RowDecision& row) {
EXPECT_EQ( return !row.blocking && !row.within_tolerance;
report.warnings.size(), })));
static_cast<std::size_t>(std::count_if(
report.rows.begin(), report.rows.end(),
[](const fesa::test::Mitc4RowDecision& row) {
return !row.blocking && !row.withinTolerance;
})));
} }
// MITC4-E2E-S4-001 // MITC4-E2E-S4-001
TEST(Mitc4S4Reference, PreservesS4AndWritesCommonMitc4Metadata) { TEST(Mitc4S4Reference, PreservesS4AndWritesCommonMitc4Metadata) {
const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
const auto directory = root / "reference" / "shell"; const auto directory = root / "reference" / "shell";
const auto evidence = runCase( const auto evidence =
"shell-s4", "S4", directory, directory / "shell.inp", RunCase("shell-s4", "S4", directory, directory / "shell.inp",
directory / "shell displacements.csv", "mitc4-shell-s4-metadata"); directory / "shell displacements.csv", "mitc4-shell-s4-metadata");
expectCommonMetadata(evidence.report, "shell-s4", "S4"); ExpectCommonMetadata(evidence.report, "shell-s4", "S4");
} }
// MITC4-E2E-S4-002 // MITC4-E2E-S4-002
TEST(Mitc4S4Reference, PassesBlockingUAndReportsEveryUrRow) { TEST(Mitc4S4Reference, PassesBlockingUAndReportsEveryUrRow) {
const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
const auto directory = root / "reference" / "shell"; const auto directory = root / "reference" / "shell";
const auto evidence = runCase( const auto evidence = RunCase(
"shell-s4", "S4", directory, directory / "shell.inp", "shell-s4", "S4", directory, directory / "shell.inp",
directory / "shell displacements.csv", "mitc4-shell-s4-comparison"); directory / "shell displacements.csv", "mitc4-shell-s4-comparison");
expectCommonMetadata(evidence.report, "shell-s4", "S4"); ExpectCommonMetadata(evidence.report, "shell-s4", "S4");
expectComparisonCoverage(evidence.report); ExpectComparisonCoverage(evidence.report);
} }
} // namespace } // namespace
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,84 @@
#ifndef FESA_TESTS_REFERENCE_MITC4_REFERENCE_COMPARISON_H_
#define FESA_TESTS_REFERENCE_MITC4_REFERENCE_COMPARISON_H_
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
#include "fesa/core/status.h"
namespace fesa::test {
struct Mitc4ReferenceCase {
std::string case_id;
std::string expected_source_element_type;
std::filesystem::path input_path;
std::filesystem::path displacement_csv_path;
std::filesystem::path results_hdf5_path;
};
struct Mitc4RowDecision {
std::string case_id;
std::string instance_name;
std::int64_t source_node_label;
std::string component;
double fesa_value;
double reference_value;
double absolute_error;
double tolerance;
double normalized_error;
bool blocking;
bool within_tolerance;
};
struct Mitc4ComponentMetrics {
std::string component;
double reference_scale;
double tolerance;
double maximum_absolute_error;
double maximum_normalized_error;
double rms_error;
double vector_norm_error;
std::size_t worst_row;
};
struct Mitc4VectorMetrics {
std::string instance_name;
std::int64_t source_node_label;
double displacement_norm_error;
double rotation_norm_error;
};
struct Mitc4Warning {
std::string code;
std::size_t row;
std::string message;
};
struct Mitc4ComparisonReport {
std::string case_id;
std::string source_element_type;
std::string internal_formulation;
std::string integration_rule;
std::vector<Mitc4RowDecision> rows;
std::vector<Mitc4ComponentMetrics> metrics;
std::vector<Mitc4VectorMetrics> vector_metrics;
std::vector<Mitc4Warning> warnings;
std::size_t worst_row;
bool passed;
};
class Mitc4ReferenceComparison {
public:
static Result<Mitc4ComparisonReport> Compare(
const Mitc4ReferenceCase& reference_case);
static Status WriteDeterministicJson(
const Mitc4ComparisonReport& report,
const std::filesystem::path& output_json);
};
} // namespace fesa::test
#endif // FESA_TESTS_REFERENCE_MITC4_REFERENCE_COMPARISON_H_
@@ -1,81 +0,0 @@
#pragma once
#include "fesa/core/status.h"
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
namespace fesa::test {
struct Mitc4ReferenceCase {
std::string caseId;
std::string expectedSourceElementType;
std::filesystem::path inputPath;
std::filesystem::path displacementCsvPath;
std::filesystem::path resultsHdf5Path;
};
struct Mitc4RowDecision {
std::string caseId;
std::string instanceName;
std::int64_t sourceNodeLabel;
std::string component;
double fesaValue;
double referenceValue;
double absoluteError;
double tolerance;
double normalizedError;
bool blocking;
bool withinTolerance;
};
struct Mitc4ComponentMetrics {
std::string component;
double referenceScale;
double tolerance;
double maximumAbsoluteError;
double maximumNormalizedError;
double rmsError;
double vectorNormError;
std::size_t worstRow;
};
struct Mitc4VectorMetrics {
std::string instanceName;
std::int64_t sourceNodeLabel;
double displacementNormError;
double rotationNormError;
};
struct Mitc4Warning {
std::string code;
std::size_t row;
std::string message;
};
struct Mitc4ComparisonReport {
std::string caseId;
std::string sourceElementType;
std::string internalFormulation;
std::string integrationRule;
std::vector<Mitc4RowDecision> rows;
std::vector<Mitc4ComponentMetrics> metrics;
std::vector<Mitc4VectorMetrics> vectorMetrics;
std::vector<Mitc4Warning> warnings;
std::size_t worstRow;
bool passed;
};
class Mitc4ReferenceComparison {
public:
static Result<Mitc4ComparisonReport> compare(
const Mitc4ReferenceCase& referenceCase);
static Status writeDeterministicJson(
const Mitc4ComparisonReport& report,
const std::filesystem::path& outputJson);
};
} // namespace fesa::test
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+81
View File
@@ -0,0 +1,81 @@
#ifndef FESA_TESTS_REFERENCE_REFERENCE_COMPARISON_H_
#define FESA_TESTS_REFERENCE_REFERENCE_COMPARISON_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
#include "fesa/core/status.h"
namespace fesa::test {
enum class ComparisonQuantity { kDisplacement, kReaction, kSectionResultant };
struct CanonicalComparisonRow {
std::string model_id;
std::string step_name;
std::size_t frame_index;
std::string instance_name;
std::int64_t source_node_label;
ComparisonQuantity quantity;
std::string component;
double value;
std::string unit_dimension;
std::string coordinate_system;
std::string hdf5_dataset_path;
};
struct RowDecision {
CanonicalComparisonRow fesa;
CanonicalComparisonRow reference;
double absolute_error;
double tolerance;
bool passed;
};
struct ComponentMetrics {
ComparisonQuantity quantity;
std::string component;
double reference_scale;
double maximum_absolute_error;
double maximum_normalized_error;
double rms_error;
double norm_error;
std::size_t worst_row;
};
struct PhysicsEvidence {
double free_residual_norm;
std::array<double, 3> applied_force;
std::array<double, 3> reaction_force;
std::array<double, 3> applied_moment_about_origin;
std::array<double, 3> reaction_moment_about_origin;
bool endpoint_consistency_passed;
};
struct ComparisonReport {
std::vector<RowDecision> rows;
std::vector<ComponentMetrics> metrics;
PhysicsEvidence physics_evidence;
bool stress_comparison_applicable;
std::string stress_comparison_reason;
bool passed;
};
// Test-only comparison support keeps Abaqus artifacts read-only and exposes no
// backend handles to the implementation or downstream verification Steps.
class ReferenceComparison {
public:
static Result<ComparisonReport> Compare(
const std::filesystem::path& results_hdf5,
const std::filesystem::path& legacy_reference_directory);
static Status WriteDeterministicJson(
const ComparisonReport& report, const std::filesystem::path& output_json);
};
} // namespace fesa::test
#endif // FESA_TESTS_REFERENCE_REFERENCE_COMPARISON_H_
-83
View File
@@ -1,83 +0,0 @@
#pragma once
#include "fesa/core/status.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
namespace fesa::test {
enum class ComparisonQuantity {
displacement,
reaction,
sectionResultant
};
struct CanonicalComparisonRow {
std::string modelId;
std::string stepName;
std::size_t frameIndex;
std::string instanceName;
std::int64_t sourceNodeLabel;
ComparisonQuantity quantity;
std::string component;
double value;
std::string unitDimension;
std::string coordinateSystem;
std::string hdf5DatasetPath;
};
struct RowDecision {
CanonicalComparisonRow fesa;
CanonicalComparisonRow reference;
double absoluteError;
double tolerance;
bool passed;
};
struct ComponentMetrics {
ComparisonQuantity quantity;
std::string component;
double referenceScale;
double maximumAbsoluteError;
double maximumNormalizedError;
double rmsError;
double normError;
std::size_t worstRow;
};
struct PhysicsEvidence {
double freeResidualNorm;
std::array<double, 3> appliedForce;
std::array<double, 3> reactionForce;
std::array<double, 3> appliedMomentAboutOrigin;
std::array<double, 3> reactionMomentAboutOrigin;
bool endpointConsistencyPassed;
};
struct ComparisonReport {
std::vector<RowDecision> rows;
std::vector<ComponentMetrics> metrics;
PhysicsEvidence physicsEvidence;
bool stressComparisonApplicable;
std::string stressComparisonReason;
bool passed;
};
// Test-only comparison support keeps Abaqus artifacts read-only and exposes no
// backend handles to the implementation or downstream verification Steps.
class ReferenceComparison {
public:
static Result<ComparisonReport> compare(
const std::filesystem::path& resultsHdf5,
const std::filesystem::path& legacyReferenceDirectory);
static Status writeDeterministicJson(
const ComparisonReport& report,
const std::filesystem::path& outputJson);
};
} // namespace fesa::test
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+87 -97
View File
@@ -1,4 +1,4 @@
#include "fesa/io/abaqus/input_reader.hpp" #include "fesa/io/abaqus/input_reader.h"
#include <gtest/gtest.h> #include <gtest/gtest.h>
@@ -12,122 +12,112 @@
namespace { namespace {
class TemporaryInputFile { class TemporaryInputFile {
public: public:
TemporaryInputFile(const std::string& stem, const std::string& content) TemporaryInputFile(const std::string& stem, const std::string& content)
: path_{std::filesystem::temp_directory_path() / : path_{std::filesystem::temp_directory_path() /
("fesa-" + stem + ".inp")} { ("fesa-" + stem + ".inp")} {
std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
stream.write(content.data(), static_cast<std::streamsize>(content.size())); stream.write(content.data(), static_cast<std::streamsize>(content.size()));
if (!stream) { if (!stream) {
throw std::runtime_error{"Unable to create INP reader test fixture."}; throw std::runtime_error{"Unable to create INP reader test fixture."};
}
} }
}
~TemporaryInputFile() { ~TemporaryInputFile() {
std::error_code error; std::error_code error;
std::filesystem::remove(path_, error); std::filesystem::remove(path_, error);
} }
const std::filesystem::path& path() const noexcept { const std::filesystem::path& Path() const noexcept { return path_; }
return path_;
}
private: private:
std::filesystem::path path_; std::filesystem::path path_;
}; };
std::string readExactBytes(const std::filesystem::path& path) { std::string ReadExactBytes(const std::filesystem::path& path) {
std::ifstream stream{path, std::ios::binary}; std::ifstream stream{path, std::ios::binary};
if (!stream) { if (!stream) {
throw std::runtime_error{"Unable to read legacy INP fixture."}; throw std::runtime_error{"Unable to read legacy INP fixture."};
} }
return std::string{ return std::string{std::istreambuf_iterator<char>{stream},
std::istreambuf_iterator<char>{stream}, std::istreambuf_iterator<char>{}};
std::istreambuf_iterator<char>{}};
} }
std::filesystem::path repositoryRoot() { std::filesystem::path RepositoryRoot() {
auto path = std::filesystem::path{__FILE__}.parent_path(); auto path = std::filesystem::path{__FILE__}.parent_path();
for (int parent = 0; parent < 4; ++parent) { for (int parent = 0; parent < 4; ++parent) {
path = path.parent_path(); path = path.parent_path();
} }
return path; return path;
} }
} // namespace } // namespace
TEST(InpSyntax, RejectsMalformedOrOrphanData) { TEST(InpSyntax, RejectsMalformedOrOrphanData) {
const auto missingPath = const auto missing_path =
std::filesystem::temp_directory_path() / "fesa-missing-input.inp"; std::filesystem::temp_directory_path() / "fesa-missing-input.inp";
std::error_code removeError; std::error_code remove_error;
std::filesystem::remove(missingPath, removeError); std::filesystem::remove(missing_path, remove_error);
const auto unreadable = fesa::AbaqusInputReader{}.read(missingPath); const auto unreadable = fesa::AbaqusInputReader{}.Read(missing_path);
ASSERT_FALSE(unreadable.HasValue()); ASSERT_FALSE(unreadable.HasValue());
EXPECT_EQ( EXPECT_EQ(unreadable.GetStatus().Category(), fesa::FailureCategory::kInput);
unreadable.GetStatus().Category(), ASSERT_EQ(unreadable.GetStatus().Diagnostics().size(), 1U);
fesa::FailureCategory::kInput); EXPECT_EQ(unreadable.GetStatus().Diagnostics()[0].code,
ASSERT_EQ(unreadable.GetStatus().Diagnostics().size(), 1U); "input-file-unreadable");
EXPECT_EQ(unreadable.GetStatus().Diagnostics()[0].code,
"input-file-unreadable");
const TemporaryInputFile malformed{"malformed-keyword", "*, name=value\n"}; const TemporaryInputFile malformed{"malformed-keyword", "*, name=value\n"};
const auto malformedResult = const auto malformed_result =
fesa::AbaqusInputReader{}.read(malformed.path()); fesa::AbaqusInputReader{}.Read(malformed.Path());
ASSERT_FALSE(malformedResult.HasValue()); ASSERT_FALSE(malformed_result.HasValue());
EXPECT_EQ( EXPECT_EQ(malformed_result.GetStatus().Category(),
malformedResult.GetStatus().Category(), fesa::FailureCategory::kInput);
fesa::FailureCategory::kInput); ASSERT_EQ(malformed_result.GetStatus().Diagnostics().size(), 1U);
ASSERT_EQ(malformedResult.GetStatus().Diagnostics().size(), 1U); EXPECT_EQ(malformed_result.GetStatus().Diagnostics()[0].code,
EXPECT_EQ(malformedResult.GetStatus().Diagnostics()[0].code, "malformed-keyword");
"malformed-keyword"); EXPECT_EQ(malformed_result.GetStatus().Diagnostics()[0].location.line, 1U);
EXPECT_EQ(malformedResult.GetStatus().Diagnostics()[0].location.line, 1U);
const TemporaryInputFile orphan{ const TemporaryInputFile orphan{"orphan-data",
"orphan-data", "** comment\n\norphan, data\n"}; "** comment\n\norphan, data\n"};
const auto orphanResult = fesa::AbaqusInputReader{}.read(orphan.path()); const auto orphan_result = fesa::AbaqusInputReader{}.Read(orphan.Path());
ASSERT_FALSE(orphanResult.HasValue()); ASSERT_FALSE(orphan_result.HasValue());
EXPECT_EQ( EXPECT_EQ(orphan_result.GetStatus().Category(),
orphanResult.GetStatus().Category(), fesa::FailureCategory::kInput);
fesa::FailureCategory::kInput); ASSERT_EQ(orphan_result.GetStatus().Diagnostics().size(), 1U);
ASSERT_EQ(orphanResult.GetStatus().Diagnostics().size(), 1U); EXPECT_EQ(orphan_result.GetStatus().Diagnostics()[0].code,
EXPECT_EQ(orphanResult.GetStatus().Diagnostics()[0].code, "orphan-data-line");
"orphan-data-line"); EXPECT_EQ(orphan_result.GetStatus().Diagnostics()[0].location.line, 3U);
EXPECT_EQ(orphanResult.GetStatus().Diagnostics()[0].location.line, 3U);
} }
TEST(InpSyntax, ReadsLegacyCantileverWithoutMutation) { TEST(InpSyntax, ReadsLegacyCantileverWithoutMutation) {
const auto inputPath = const auto input_path = RepositoryRoot() / "reference" / "cantilever beam" /
repositoryRoot() / "reference" / "cantilever beam" / "cantilever beam.inp";
"cantilever beam.inp"; const auto bytes_before = ReadExactBytes(input_path);
const auto bytesBefore = readExactBytes(inputPath); const auto timestamp_before = std::filesystem::last_write_time(input_path);
const auto timestampBefore = std::filesystem::last_write_time(inputPath);
const auto result = fesa::AbaqusInputReader{}.read(inputPath); const auto result = fesa::AbaqusInputReader{}.Read(input_path);
ASSERT_TRUE(result.HasValue()); ASSERT_TRUE(result.HasValue());
EXPECT_EQ(result.Value().sourceContentIdentity, EXPECT_EQ(result.Value().source_content_identity, "fnv1a64:04543464cc970405");
"fnv1a64:04543464cc970405"); EXPECT_EQ(result.Value().source_path,
EXPECT_EQ(result.Value().sourcePath, std::filesystem::absolute(input_path).lexically_normal());
std::filesystem::absolute(inputPath).lexically_normal()); ASSERT_EQ(result.Value().blocks.size(), 30U);
ASSERT_EQ(result.Value().blocks.size(), 30U); EXPECT_EQ(result.Value().blocks.front().canonical_name, "HEADING");
EXPECT_EQ(result.Value().blocks.front().canonicalName, "HEADING"); EXPECT_EQ(result.Value().blocks.front().location.line, 1U);
EXPECT_EQ(result.Value().blocks.front().location.line, 1U); EXPECT_EQ(result.Value().blocks.back().canonical_name, "END STEP");
EXPECT_EQ(result.Value().blocks.back().canonicalName, "END STEP");
const auto element = std::find_if( const auto element =
result.Value().blocks.begin(), std::find_if(result.Value().blocks.begin(), result.Value().blocks.end(),
result.Value().blocks.end(), [](const fesa::KeywordBlock& block) {
[](const fesa::KeywordBlock& block) { return block.canonical_name == "ELEMENT";
return block.canonicalName == "ELEMENT"; });
}); ASSERT_NE(element, result.Value().blocks.end());
ASSERT_NE(element, result.Value().blocks.end()); ASSERT_EQ(element->parameters.size(), 1U);
ASSERT_EQ(element->parameters.size(), 1U); EXPECT_EQ(element->parameters[0].name, "TYPE");
EXPECT_EQ(element->parameters[0].name, "TYPE"); ASSERT_TRUE(element->parameters[0].value.has_value());
ASSERT_TRUE(element->parameters[0].value.has_value()); EXPECT_EQ(*element->parameters[0].value, "B33");
EXPECT_EQ(*element->parameters[0].value, "B33"); EXPECT_EQ(element->data.size(), 10U);
EXPECT_EQ(element->data.size(), 10U);
EXPECT_EQ(readExactBytes(inputPath), bytesBefore); EXPECT_EQ(ReadExactBytes(input_path), bytes_before);
EXPECT_EQ(std::filesystem::last_write_time(inputPath), timestampBefore); EXPECT_EQ(std::filesystem::last_write_time(input_path), timestamp_before);
} }
+60 -66
View File
@@ -1,5 +1,3 @@
#include "fesa/io/abaqus/input_reader.hpp"
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <filesystem> #include <filesystem>
@@ -8,88 +6,84 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "fesa/io/abaqus/input_reader.h"
namespace { namespace {
class TemporaryInputFile { class TemporaryInputFile {
public: public:
TemporaryInputFile(const std::string& stem, const std::string& content) TemporaryInputFile(const std::string& stem, const std::string& content)
: path_{std::filesystem::temp_directory_path() / : path_{std::filesystem::temp_directory_path() /
("fesa-" + stem + ".inp")} { ("fesa-" + stem + ".inp")} {
std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
stream.write(content.data(), static_cast<std::streamsize>(content.size())); stream.write(content.data(), static_cast<std::streamsize>(content.size()));
if (!stream) { if (!stream) {
throw std::runtime_error{"Unable to create INP syntax test fixture."}; throw std::runtime_error{"Unable to create INP syntax test fixture."};
}
} }
}
~TemporaryInputFile() { ~TemporaryInputFile() {
std::error_code error; std::error_code error;
std::filesystem::remove(path_, error); std::filesystem::remove(path_, error);
} }
const std::filesystem::path& path() const noexcept { const std::filesystem::path& Path() const noexcept { return path_; }
return path_;
}
private: private:
std::filesystem::path path_; std::filesystem::path path_;
}; };
} // namespace } // namespace
TEST(InpSyntax, CanonicalizesKeywordAndParameterNamesOnly) { TEST(InpSyntax, CanonicalizesKeywordAndParameterNamesOnly) {
const std::string originalLine = const std::string original_line =
" *eLeMeNt, TyPe= b33 , generate, ELSET=Beam_Set "; " *eLeMeNt, TyPe= b33 , generate, ELSET=Beam_Set ";
const TemporaryInputFile input{ const TemporaryInputFile input{"canonical-names", original_line + "\n"};
"canonical-names", originalLine + "\n"};
const auto result = fesa::AbaqusInputReader{}.read(input.path()); const auto result = fesa::AbaqusInputReader{}.Read(input.Path());
ASSERT_TRUE(result.HasValue()); ASSERT_TRUE(result.HasValue());
ASSERT_EQ(result.Value().blocks.size(), 1U); ASSERT_EQ(result.Value().blocks.size(), 1U);
const auto& block = result.Value().blocks[0]; const auto& block = result.Value().blocks[0];
EXPECT_EQ(block.canonicalName, "ELEMENT"); EXPECT_EQ(block.canonical_name, "ELEMENT");
EXPECT_EQ(block.originalLine, originalLine); EXPECT_EQ(block.original_line, original_line);
EXPECT_EQ(block.location.line, 1U); EXPECT_EQ(block.location.line, 1U);
ASSERT_EQ(block.parameters.size(), 3U); ASSERT_EQ(block.parameters.size(), 3U);
EXPECT_EQ(block.parameters[0].name, "TYPE"); EXPECT_EQ(block.parameters[0].name, "TYPE");
ASSERT_TRUE(block.parameters[0].value.has_value()); ASSERT_TRUE(block.parameters[0].value.has_value());
EXPECT_EQ(*block.parameters[0].value, "b33"); EXPECT_EQ(*block.parameters[0].value, "b33");
EXPECT_EQ(block.parameters[1].name, "GENERATE"); EXPECT_EQ(block.parameters[1].name, "GENERATE");
EXPECT_FALSE(block.parameters[1].value.has_value()); EXPECT_FALSE(block.parameters[1].value.has_value());
EXPECT_EQ(block.parameters[2].name, "ELSET"); EXPECT_EQ(block.parameters[2].name, "ELSET");
ASSERT_TRUE(block.parameters[2].value.has_value()); ASSERT_TRUE(block.parameters[2].value.has_value());
EXPECT_EQ(*block.parameters[2].value, "Beam_Set"); EXPECT_EQ(*block.parameters[2].value, "Beam_Set");
} }
TEST(InpSyntax, PreservesDataAndSourceLocations) { TEST(InpSyntax, PreservesDataAndSourceLocations) {
const std::string exactBytes = const std::string exact_bytes =
"** retained only in line accounting\r\n" "** retained only in line accounting\r\n"
"\r\n" "\r\n"
"*NoDe\r\n" "*NoDe\r\n"
" 0007, Label_A, ,\r\n"; " 0007, Label_A, ,\r\n";
const TemporaryInputFile input{"data-and-locations", exactBytes}; const TemporaryInputFile input{"data-and-locations", exact_bytes};
const auto result = fesa::AbaqusInputReader{}.read(input.path()); const auto result = fesa::AbaqusInputReader{}.Read(input.Path());
ASSERT_TRUE(result.HasValue()); ASSERT_TRUE(result.HasValue());
EXPECT_EQ( EXPECT_EQ(result.Value().source_path,
result.Value().sourcePath, std::filesystem::absolute(input.Path()).lexically_normal());
std::filesystem::absolute(input.path()).lexically_normal()); EXPECT_EQ(result.Value().source_content_identity, "fnv1a64:c120b6ed2445be46");
EXPECT_EQ(result.Value().sourceContentIdentity, ASSERT_EQ(result.Value().blocks.size(), 1U);
"fnv1a64:c120b6ed2445be46"); const auto& block = result.Value().blocks[0];
ASSERT_EQ(result.Value().blocks.size(), 1U); EXPECT_EQ(block.canonical_name, "NODE");
const auto& block = result.Value().blocks[0]; EXPECT_EQ(block.original_line, "*NoDe");
EXPECT_EQ(block.canonicalName, "NODE"); EXPECT_EQ(block.location.file, result.Value().source_path);
EXPECT_EQ(block.originalLine, "*NoDe"); EXPECT_EQ(block.location.line, 3U);
EXPECT_EQ(block.location.file, result.Value().sourcePath);
EXPECT_EQ(block.location.line, 3U);
ASSERT_EQ(block.data.size(), 1U); ASSERT_EQ(block.data.size(), 1U);
EXPECT_EQ( EXPECT_EQ(block.data[0].fields,
block.data[0].fields, (std::vector<std::string>{"0007", "Label_A", "", ""}));
(std::vector<std::string>{"0007", "Label_A", "", ""})); EXPECT_EQ(block.data[0].location.file, result.Value().source_path);
EXPECT_EQ(block.data[0].location.file, result.Value().sourcePath); EXPECT_EQ(block.data[0].location.line, 4U);
EXPECT_EQ(block.data[0].location.line, 4U);
} }
File diff suppressed because it is too large Load Diff