feat(nonlinear-static-newton-raphson): step 7 - newton-iteration-log-writer

This commit is contained in:
KOKO\Mimi
2026-08-24 14:34:19 +09:00
parent b0d53fb8f8
commit 902bc79e16
5 changed files with 746 additions and 0 deletions
@@ -0,0 +1,89 @@
#ifndef FESA_ANALYSIS_NEWTON_ITERATION_LOG_H_
#define FESA_ANALYSIS_NEWTON_ITERATION_LOG_H_
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include "fesa/analysis/nonlinear_static_control.h"
#include "fesa/core/status.h"
namespace fesa {
/// @brief Identifies the latest Newton procedure stage reached by a record.
enum class NewtonIterationStage {
kEntered,
kKinematicsEvaluated,
kInternalAssembled,
kExternalAssembled,
kSolvedCorrection,
kConvergenceEvaluated,
kAccepted,
kFailed,
};
/// @brief Carries the controlled failure that ended one Newton iteration.
struct NewtonIterationFailure {
bool present;
NewtonIterationStage stage;
std::string diagnostic_code;
std::string reason;
};
/// @brief Carries one fixed-schema Newton iteration diagnostic record.
struct NewtonIterationLogRecord {
std::string step_name;
std::uint32_t increment_index;
double load_factor;
std::uint32_t iteration;
NewtonIterationStage last_completed_stage;
NewtonIterationFailure failure;
std::string transition;
std::string outcome;
std::string diagnostic_code;
NewtonChannelDecision force_residual;
NewtonChannelDecision moment_residual;
NewtonChannelDecision translation_correction;
NewtonChannelDecision rotation_correction;
bool combined_passed;
};
/// @brief Writes deterministic, line-oriented Newton iteration diagnostics.
/// @note A write, flush, or close failure permanently prevents a successful
/// log lifecycle for this writer instance.
class NewtonIterationLogWriter {
public:
/// @brief Opens and truncates the diagnostic log before Newton execution.
/// @param log_path Derived application-owned diagnostic-log path.
/// @return Success when the writer owns an empty binary log, otherwise an
/// output failure with code `log-output-failure`.
Status Open(const std::filesystem::path& log_path);
/// @brief Writes one complete LF-terminated Newton iteration record.
/// @param record Explicit stage, outcome, and typed convergence decision.
/// @return Success after writing a complete record, otherwise an output
/// failure with code `log-output-failure`.
Status Write(const NewtonIterationLogRecord& record);
/// @brief Flushes every complete record written so far to the log.
/// @return Success after the stream flush, otherwise an output failure with
/// code `log-output-failure`.
Status Flush();
/// @brief Closes the log before authoritative result finalization.
/// @return Success after close, otherwise an output failure with code
/// `log-output-failure`.
Status Close();
private:
enum class State { kReady, kOpen, kClosed, kFailed };
std::ofstream stream_;
std::filesystem::path log_path_;
State state_{State::kReady};
};
} // namespace fesa
#endif // FESA_ANALYSIS_NEWTON_ITERATION_LOG_H_
+1
View File
@@ -4,6 +4,7 @@ add_library(
analysis/analysis_model.cpp
analysis/analysis_state.cpp
analysis/linear_static_analysis.cpp
analysis/newton_iteration_log.cpp
analysis/nonlinear_static_control.cpp
app/fesa_application.cpp
app/output_paths.cpp
+328
View File
@@ -0,0 +1,328 @@
#include "fesa/analysis/newton_iteration_log.h"
#include <array>
#include <charconv>
#include <cmath>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace fesa {
namespace {
struct ChannelSpec {
std::string_view label_prefix;
std::string_view identity;
};
Status LogOutputFailure(const std::filesystem::path& log_path,
const std::string& message) {
return Status::Failure(
FailureCategory::kOutput,
{{Severity::kError,
"log-output-failure",
{{}, 0U},
"",
log_path.generic_u8string(),
message}});
}
std::string EscapeText(const std::string_view value) {
constexpr char kHexDigits[] = "0123456789ABCDEF";
std::string escaped;
escaped.reserve(value.size());
for (const unsigned char byte : value) {
switch (byte) {
case '\\':
escaped += "\\\\";
break;
case '\t':
escaped += "\\t";
break;
case '\n':
escaped += "\\n";
break;
case '\r':
escaped += "\\r";
break;
case '\0':
escaped += "\\0";
break;
default:
if (byte < 0x20U || byte == 0x7FU) {
escaped += "\\x";
escaped += kHexDigits[byte >> 4U];
escaped += kHexDigits[byte & 0x0FU];
} else {
escaped += static_cast<char>(byte);
}
break;
}
}
return escaped;
}
Result<std::string> FormatFiniteNumber(const double value,
const std::filesystem::path& log_path) {
if (!std::isfinite(value)) {
return Result<std::string>::Failure(
LogOutputFailure(log_path, "A Newton log numeric value is nonfinite."));
}
std::array<char, 64U> buffer{};
const auto conversion = std::to_chars(
buffer.data(), buffer.data() + buffer.size(), value,
std::chars_format::general, std::numeric_limits<double>::max_digits10);
if (conversion.ec != std::errc{}) {
return Result<std::string>::Failure(
LogOutputFailure(log_path, "A Newton log numeric value could not format."));
}
return Result<std::string>::Success(
std::string{buffer.data(), conversion.ptr});
}
Result<std::string_view> BranchLabel(
const NewtonConvergenceBranch branch,
const std::filesystem::path& log_path) {
switch (branch) {
case NewtonConvergenceBranch::kRelative:
return Result<std::string_view>::Success("relative");
case NewtonConvergenceBranch::kZeroScale:
return Result<std::string_view>::Success("zero-scale");
}
return Result<std::string_view>::Failure(LogOutputFailure(
log_path, "A Newton log convergence branch is not supported."));
}
Result<std::string_view> UnitLabel(const NewtonChannelUnit unit,
const std::filesystem::path& log_path) {
switch (unit) {
case NewtonChannelUnit::kForce:
return Result<std::string_view>::Success("force");
case NewtonChannelUnit::kForceLength:
return Result<std::string_view>::Success("force*length");
case NewtonChannelUnit::kLength:
return Result<std::string_view>::Success("length");
case NewtonChannelUnit::kDimensionless:
return Result<std::string_view>::Success("1");
}
return Result<std::string_view>::Failure(LogOutputFailure(
log_path, "A Newton log threshold unit is not supported."));
}
std::string StageLabel(const NewtonIterationStage stage) {
switch (stage) {
case NewtonIterationStage::kEntered:
return "iteration-entered";
case NewtonIterationStage::kKinematicsEvaluated:
return "trial-state";
case NewtonIterationStage::kInternalAssembled:
return "residual";
case NewtonIterationStage::kExternalAssembled:
return "load-boundary";
case NewtonIterationStage::kSolvedCorrection:
return "correction";
case NewtonIterationStage::kConvergenceEvaluated:
return "convergence-gates";
case NewtonIterationStage::kAccepted:
return "commit";
case NewtonIterationStage::kFailed:
return "failed";
}
return "failed";
}
bool AreChannelsEvaluated(const NewtonIterationLogRecord& record) {
if (!record.failure.present) {
return true;
}
return record.last_completed_stage ==
NewtonIterationStage::kConvergenceEvaluated ||
record.last_completed_stage == NewtonIterationStage::kAccepted;
}
void AppendField(std::vector<std::string>* fields, const std::string_view label,
const std::string& value) {
fields->push_back(std::string{label} + "=" + value);
}
Status AppendChannelFields(
std::vector<std::string>* fields, const ChannelSpec spec,
const NewtonChannelDecision& decision, const bool evaluated,
const std::string& not_evaluated, const std::filesystem::path& log_path) {
const std::string prefix{spec.label_prefix};
if (!evaluated) {
for (const std::string_view suffix :
{"channel", "numerator_norm", "denominator_scale", "branch",
"threshold", "threshold_unit", "pass"}) {
AppendField(fields, prefix + "_" + std::string{suffix}, not_evaluated);
}
return Status::Ok();
}
if (decision.identity != spec.identity) {
return LogOutputFailure(log_path,
"A Newton log channel identity is invalid.");
}
const auto numerator = FormatFiniteNumber(decision.numerator_norm, log_path);
if (!numerator.HasValue()) {
return numerator.GetStatus();
}
const auto denominator =
FormatFiniteNumber(decision.denominator_scale, log_path);
if (!denominator.HasValue()) {
return denominator.GetStatus();
}
const auto branch = BranchLabel(decision.branch, log_path);
if (!branch.HasValue()) {
return branch.GetStatus();
}
const auto threshold = FormatFiniteNumber(decision.threshold, log_path);
if (!threshold.HasValue()) {
return threshold.GetStatus();
}
const auto unit = UnitLabel(decision.threshold_unit, log_path);
if (!unit.HasValue()) {
return unit.GetStatus();
}
AppendField(fields, prefix + "_channel", decision.identity);
AppendField(fields, prefix + "_numerator_norm", numerator.Value());
AppendField(fields, prefix + "_denominator_scale", denominator.Value());
AppendField(fields, prefix + "_branch", std::string{branch.Value()});
AppendField(fields, prefix + "_threshold", threshold.Value());
AppendField(fields, prefix + "_threshold_unit", std::string{unit.Value()});
AppendField(fields, prefix + "_pass", decision.passed ? "1" : "0");
return Status::Ok();
}
Result<std::string> SerializeRecord(const NewtonIterationLogRecord& record,
const std::filesystem::path& log_path) {
const auto load_factor = FormatFiniteNumber(record.load_factor, log_path);
if (!load_factor.HasValue()) {
return Result<std::string>::Failure(load_factor.GetStatus());
}
const bool channels_evaluated = AreChannelsEvaluated(record);
const std::string not_evaluated =
"not-evaluated(" + EscapeText(record.failure.reason) + ")";
std::vector<std::string> fields;
fields.reserve(39U);
AppendField(&fields, "record", "newton-iteration");
AppendField(&fields, "schema", "nsnr-log-v1");
AppendField(&fields, "increment", std::to_string(record.increment_index));
AppendField(&fields, "target_factor", load_factor.Value());
AppendField(&fields, "iteration", std::to_string(record.iteration));
AppendField(&fields, "last_completed_stage",
StageLabel(record.last_completed_stage));
AppendField(&fields, "failing_stage",
record.failure.present ? StageLabel(record.failure.stage) : "none");
const std::array<ChannelSpec, 4U> specs{{
{"residual_force", "residual_force"},
{"residual_moment", "residual_moment"},
{"correction_translation", "correction_translation"},
{"correction_rotation", "correction_rotation"},
}};
const std::array<const NewtonChannelDecision*, 4U> decisions{{
&record.force_residual,
&record.moment_residual,
&record.translation_correction,
&record.rotation_correction,
}};
for (std::size_t index = 0U; index < specs.size(); ++index) {
const Status channel_status =
AppendChannelFields(&fields, specs[index], *decisions[index],
channels_evaluated, not_evaluated, log_path);
if (!channel_status.IsOk()) {
return Result<std::string>::Failure(channel_status);
}
}
AppendField(&fields, "combined_pass",
channels_evaluated ? (record.combined_passed ? "1" : "0")
: not_evaluated);
AppendField(&fields, "transition", EscapeText(record.transition));
AppendField(&fields, "outcome", EscapeText(record.outcome));
AppendField(&fields, "diagnostic_code", EscapeText(record.diagnostic_code));
std::string line;
for (std::size_t index = 0U; index < fields.size(); ++index) {
if (index != 0U) {
line += '\t';
}
line += fields[index];
}
line += '\n';
return Result<std::string>::Success(std::move(line));
}
} // namespace
Status NewtonIterationLogWriter::Open(const std::filesystem::path& log_path) {
if (state_ != State::kReady) {
state_ = State::kFailed;
return LogOutputFailure(log_path_, "The Newton log writer is not ready.");
}
log_path_ = log_path;
stream_.open(log_path_, std::ios::binary | std::ios::out | std::ios::trunc);
if (!stream_.is_open() || !stream_) {
state_ = State::kFailed;
return LogOutputFailure(log_path_, "The Newton log could not open.");
}
state_ = State::kOpen;
return Status::Ok();
}
Status NewtonIterationLogWriter::Write(const NewtonIterationLogRecord& record) {
if (state_ != State::kOpen) {
state_ = State::kFailed;
return LogOutputFailure(log_path_, "The Newton log writer is not open.");
}
const auto line = SerializeRecord(record, log_path_);
if (!line.HasValue()) {
state_ = State::kFailed;
return line.GetStatus();
}
stream_.write(line.Value().data(),
static_cast<std::streamsize>(line.Value().size()));
if (!stream_) {
state_ = State::kFailed;
return LogOutputFailure(log_path_, "The Newton log record could not write.");
}
return Status::Ok();
}
Status NewtonIterationLogWriter::Flush() {
if (state_ != State::kOpen) {
state_ = State::kFailed;
return LogOutputFailure(log_path_, "The Newton log writer is not open.");
}
stream_.flush();
if (!stream_) {
state_ = State::kFailed;
return LogOutputFailure(log_path_, "The Newton log could not flush.");
}
return Status::Ok();
}
Status NewtonIterationLogWriter::Close() {
if (state_ != State::kOpen) {
state_ = State::kFailed;
return LogOutputFailure(log_path_, "The Newton log writer is not open.");
}
stream_.close();
if (stream_.fail()) {
state_ = State::kFailed;
return LogOutputFailure(log_path_, "The Newton log could not close.");
}
state_ = State::kClosed;
return Status::Ok();
}
} // namespace fesa
+1
View File
@@ -7,6 +7,7 @@ add_executable(
unit/analysis/analysis_model_test.cpp
unit/analysis/analysis_state_test.cpp
unit/analysis/nonlinear_static_control_test.cpp
unit/analysis/newton_iteration_log_test.cpp
unit/assembly/parallel_for_test.cpp
unit/assembly/load_assembler_test.cpp
unit/assembly/sparse_assembler_test.cpp
@@ -0,0 +1,327 @@
#include "fesa/analysis/newton_iteration_log.h"
#include <algorithm>
#include <charconv>
#include <chrono>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
#include <gtest/gtest.h>
namespace {
class TempDirectory {
public:
explicit TempDirectory(const std::string& label) {
const auto tick =
std::chrono::steady_clock::now().time_since_epoch().count();
path_ = std::filesystem::temp_directory_path() /
("fesa-newton-log-" + label + "-" + std::to_string(tick));
std::error_code error;
if (!std::filesystem::create_directory(path_, error) || error) {
throw std::runtime_error{"Unable to create Newton log test fixture."};
}
}
TempDirectory(const TempDirectory&) = delete;
TempDirectory& operator=(const TempDirectory&) = delete;
~TempDirectory() {
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
}
const std::filesystem::path& Path() const noexcept { return path_; }
private:
std::filesystem::path path_;
};
void ExpectLogOutputFailure(const fesa::Status& status) {
ASSERT_FALSE(status.IsOk());
ASSERT_EQ(status.Category(), fesa::FailureCategory::kOutput);
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics().front().code, "log-output-failure");
}
std::vector<std::string> SplitTabs(const std::string& line) {
std::vector<std::string> fields;
std::size_t start = 0U;
while (start <= line.size()) {
const std::size_t end = line.find('\t', start);
if (end == std::string::npos) {
fields.push_back(line.substr(start));
return fields;
}
fields.push_back(line.substr(start, end - start));
start = end + 1U;
}
return fields;
}
const std::string& FieldValue(const std::vector<std::string>& fields,
const std::string_view label) {
const std::string prefix = std::string{label} + "=";
for (const std::string& field : fields) {
if (field.compare(0U, prefix.size(), prefix) == 0) {
return field;
}
}
throw std::runtime_error{"Newton log field is missing."};
}
std::string ValueAfterEquals(const std::string& field) {
return field.substr(field.find('=') + 1U);
}
fesa::NewtonChannelDecision MakeDecision(const std::string& identity) {
return {identity,
0.0012345678901234567,
1.2345678901234567,
fesa::NewtonConvergenceBranch::kRelative,
fesa::kNewtonRelativeThreshold,
fesa::NewtonChannelUnit::kDimensionless,
true};
}
fesa::NewtonIterationLogRecord CompleteRecord() {
return {"Step-1",
3U,
0.12345678901234567,
2U,
fesa::NewtonIterationStage::kConvergenceEvaluated,
{false, fesa::NewtonIterationStage::kFailed, "", ""},
"accept",
"accepted",
"none",
MakeDecision("residual_force"),
MakeDecision("residual_moment"),
MakeDecision("correction_translation"),
MakeDecision("correction_rotation"),
true};
}
std::vector<std::string> WriteAndRead(
const std::filesystem::path& path,
const fesa::NewtonIterationLogRecord& record) {
fesa::NewtonIterationLogWriter writer;
if (!writer.Open(path).IsOk() || !writer.Write(record).IsOk() ||
!writer.Flush().IsOk() || !writer.Close().IsOk()) {
throw std::runtime_error{"Unable to write Newton log test record."};
}
std::ifstream input{path, std::ios::binary};
if (!input.is_open()) {
throw std::runtime_error{"Unable to read Newton log test record."};
}
const std::string contents{std::istreambuf_iterator<char>{input},
std::istreambuf_iterator<char>{}};
if (contents.empty()) {
throw std::runtime_error{"Newton log test record is empty."};
}
EXPECT_EQ(contents.back(), '\n');
EXPECT_EQ(std::count(contents.begin(), contents.end(), '\n'), 1);
EXPECT_EQ(contents.find('\r'), std::string::npos);
EXPECT_FALSE(contents.size() >= 3U &&
static_cast<unsigned char>(contents[0U]) == 0xEFU &&
static_cast<unsigned char>(contents[1U]) == 0xBBU &&
static_cast<unsigned char>(contents[2U]) == 0xBFU);
return SplitTabs(contents.substr(0U, contents.size() - 1U));
}
// NSNR-LOG-001, NSNR-LOG-002, and NSNR-LOG-003
TEST(NewtonIterationLog, WritesOneLfTerminated39FieldRecordInApprovedOrder) {
TempDirectory directory{"complete"};
const auto fields =
WriteAndRead(directory.Path() / "model_log.txt", CompleteRecord());
const std::vector<std::string> labels{
"record", "schema", "increment", "target_factor", "iteration",
"last_completed_stage", "failing_stage", "residual_force_channel",
"residual_force_numerator_norm", "residual_force_denominator_scale",
"residual_force_branch", "residual_force_threshold",
"residual_force_threshold_unit", "residual_force_pass",
"residual_moment_channel", "residual_moment_numerator_norm",
"residual_moment_denominator_scale", "residual_moment_branch",
"residual_moment_threshold", "residual_moment_threshold_unit",
"residual_moment_pass", "correction_translation_channel",
"correction_translation_numerator_norm",
"correction_translation_denominator_scale",
"correction_translation_branch", "correction_translation_threshold",
"correction_translation_threshold_unit", "correction_translation_pass",
"correction_rotation_channel", "correction_rotation_numerator_norm",
"correction_rotation_denominator_scale", "correction_rotation_branch",
"correction_rotation_threshold", "correction_rotation_threshold_unit",
"correction_rotation_pass", "combined_pass", "transition", "outcome",
"diagnostic_code"};
ASSERT_EQ(fields.size(), labels.size());
for (std::size_t index = 0U; index < labels.size(); ++index) {
EXPECT_EQ(fields[index].substr(0U, fields[index].find('=')), labels[index]);
}
EXPECT_EQ(FieldValue(fields, "record"), "record=newton-iteration");
EXPECT_EQ(FieldValue(fields, "schema"), "schema=nsnr-log-v1");
}
// NSNR-LOG-004
TEST(NewtonIterationLog, FormatsFiniteNumbersForLocaleIndependentRoundTrip) {
TempDirectory directory{"numbers"};
const auto fields =
WriteAndRead(directory.Path() / "model_log.txt", CompleteRecord());
const std::string value = ValueAfterEquals(FieldValue(fields, "target_factor"));
double parsed = 0.0;
const auto result = std::from_chars(value.data(), value.data() + value.size(),
parsed, std::chars_format::general);
EXPECT_EQ(result.ec, std::errc{});
EXPECT_EQ(result.ptr, value.data() + value.size());
EXPECT_DOUBLE_EQ(parsed, 0.12345678901234567);
EXPECT_NE(value.find('.'), std::string::npos);
EXPECT_EQ(value.find(','), std::string::npos);
}
// NSNR-LOG-005 and NSNR-LOG-007
TEST(NewtonIterationLog, SerializesChannelDecisionsAndExplicitOutcomeFields) {
TempDirectory directory{"channels"};
const auto fields =
WriteAndRead(directory.Path() / "model_log.txt", CompleteRecord());
for (const std::string_view prefix :
{"residual_force", "residual_moment", "correction_translation",
"correction_rotation"}) {
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_channel"),
std::string{prefix} + "_channel=" + std::string{prefix});
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_numerator_norm"),
std::string{prefix} +
"_numerator_norm=0.0012345678901234567");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_denominator_scale"),
std::string{prefix} +
"_denominator_scale=1.2345678901234567");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_branch"),
std::string{prefix} + "_branch=relative");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_threshold"),
std::string{prefix} + "_threshold=0.01");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_threshold_unit"),
std::string{prefix} + "_threshold_unit=1");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_pass"),
std::string{prefix} + "_pass=1");
}
EXPECT_EQ(FieldValue(fields, "last_completed_stage"),
"last_completed_stage=convergence-gates");
EXPECT_EQ(FieldValue(fields, "failing_stage"), "failing_stage=none");
EXPECT_EQ(FieldValue(fields, "combined_pass"), "combined_pass=1");
EXPECT_EQ(FieldValue(fields, "transition"), "transition=accept");
EXPECT_EQ(FieldValue(fields, "outcome"), "outcome=accepted");
EXPECT_EQ(FieldValue(fields, "diagnostic_code"), "diagnostic_code=none");
}
// NSNR-LOG-006
TEST(NewtonIterationLog, SerializesEmptyChannelsAsTypedZeroScaleDecisions) {
TempDirectory directory{"zero-scale"};
auto record = CompleteRecord();
const std::vector<fesa::NewtonChannelUnit> units{
fesa::NewtonChannelUnit::kForce, fesa::NewtonChannelUnit::kForceLength,
fesa::NewtonChannelUnit::kLength, fesa::NewtonChannelUnit::kDimensionless};
fesa::NewtonChannelDecision* decisions[]{
&record.force_residual, &record.moment_residual,
&record.translation_correction, &record.rotation_correction};
for (std::size_t index = 0U; index < std::size(decisions); ++index) {
decisions[index]->numerator_norm = 0.0;
decisions[index]->denominator_scale = 0.0;
decisions[index]->branch = fesa::NewtonConvergenceBranch::kZeroScale;
decisions[index]->threshold = fesa::kNewtonZeroScaleThreshold;
decisions[index]->threshold_unit = units[index];
decisions[index]->passed = true;
}
const auto fields = WriteAndRead(directory.Path() / "model_log.txt", record);
for (const std::string_view prefix :
{"residual_force", "residual_moment", "correction_translation",
"correction_rotation"}) {
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_numerator_norm"),
std::string{prefix} + "_numerator_norm=0");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_denominator_scale"),
std::string{prefix} + "_denominator_scale=0");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_branch"),
std::string{prefix} + "_branch=zero-scale");
EXPECT_EQ(FieldValue(fields, std::string{prefix} + "_threshold"),
std::string{prefix} + "_threshold=0.001");
}
EXPECT_EQ(FieldValue(fields, "residual_force_threshold_unit"),
"residual_force_threshold_unit=force");
EXPECT_EQ(FieldValue(fields, "residual_moment_threshold_unit"),
"residual_moment_threshold_unit=force*length");
EXPECT_EQ(FieldValue(fields, "correction_translation_threshold_unit"),
"correction_translation_threshold_unit=length");
EXPECT_EQ(FieldValue(fields, "correction_rotation_threshold_unit"),
"correction_rotation_threshold_unit=1");
}
// NSNR-LOG-008 and NSNR-LOG-009
TEST(NewtonIterationLog, EscapesStringsAndOmitsUnevaluatedFailureFields) {
TempDirectory directory{"failure"};
auto record = CompleteRecord();
record.last_completed_stage = fesa::NewtonIterationStage::kInternalAssembled;
record.failure = {true,
fesa::NewtonIterationStage::kExternalAssembled,
"load\\failure",
std::string{"external\tload\nmissing\r"} + '\0'};
record.transition = "fail\ttransition";
record.outcome = "failed\nrecord";
record.diagnostic_code = std::string{"external\\load\r"} + '\0';
const auto fields = WriteAndRead(directory.Path() / "model_log.txt", record);
const std::string not_evaluated =
"not-evaluated(external\\tload\\nmissing\\r\\0)";
for (std::size_t index = 7U; index < 36U; ++index) {
EXPECT_EQ(ValueAfterEquals(fields[index]), not_evaluated);
}
EXPECT_EQ(FieldValue(fields, "failing_stage"),
"failing_stage=load-boundary");
EXPECT_EQ(FieldValue(fields, "transition"),
"transition=fail\\ttransition");
EXPECT_EQ(FieldValue(fields, "outcome"), "outcome=failed\\nrecord");
EXPECT_EQ(FieldValue(fields, "diagnostic_code"),
"diagnostic_code=external\\\\load\\r\\0");
}
// NSNR-LOG-010
TEST(NewtonIterationLog, ReportsWriteFlushAndCloseStateFailures) {
TempDirectory directory{"failures"};
const auto path = directory.Path() / "model_log.txt";
fesa::NewtonIterationLogWriter writer;
ExpectLogOutputFailure(writer.Write(CompleteRecord()));
ExpectLogOutputFailure(writer.Flush());
ExpectLogOutputFailure(writer.Close());
fesa::NewtonIterationLogWriter invalid_open_writer;
ExpectLogOutputFailure(
invalid_open_writer.Open(directory.Path() / "missing" / "log.txt"));
fesa::NewtonIterationLogWriter closed_writer;
ASSERT_TRUE(closed_writer.Open(path).IsOk());
ASSERT_TRUE(closed_writer.Close().IsOk());
ExpectLogOutputFailure(closed_writer.Write(CompleteRecord()));
ExpectLogOutputFailure(closed_writer.Flush());
ExpectLogOutputFailure(closed_writer.Close());
}
// NSNR-LOG-011
TEST(NewtonIterationLog, LeavesZeroByteLogForNonNewtonControlledFailurePath) {
TempDirectory directory{"zero-byte"};
const auto path = directory.Path() / "model_log.txt";
fesa::NewtonIterationLogWriter writer;
ASSERT_TRUE(writer.Open(path).IsOk());
ASSERT_TRUE(writer.Close().IsOk());
EXPECT_TRUE(std::filesystem::exists(path));
EXPECT_EQ(std::filesystem::file_size(path), 0U);
}
} // namespace