Connect HHT field convergence to production acceptance and v2 logs
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -562,4 +563,149 @@ TEST(DynamicNewtonIterationLog,
|
||||
ExpectLogOutputFailure(unopened.Write(CompleteDynamicRecord()));
|
||||
}
|
||||
|
||||
TEST(DynamicFieldIterationLog, KeepsOrderedUnitsMissingEvidenceAndEscapedSourceName) {
|
||||
TempDirectory directory{"field-v2"};
|
||||
const auto path = directory.Path() / "log.txt";
|
||||
auto record = CompleteDynamicRecord();
|
||||
record.policy_id = "abaqus2025-field-on-fesa-v1";
|
||||
record.step_name = "\xED\x95\x9C\xEA\xB8\x80=Step\\tail\tline\nend\r";
|
||||
record.stage = "evaluation";
|
||||
record.outcome = "candidate";
|
||||
record.fields[0].residual_numerator = 1.2345678901234567;
|
||||
record.fields[0].increment_norm = 0;
|
||||
fesa::NewtonIterationLogWriter writer;
|
||||
ASSERT_TRUE(writer.Open(path).IsOk());
|
||||
ASSERT_TRUE(writer.Write(record).IsOk());
|
||||
ASSERT_TRUE(writer.Close().IsOk());
|
||||
const auto text = ReadLogBytes(path);
|
||||
EXPECT_EQ(std::count(text.begin(), text.end(), '\n'), 1);
|
||||
EXPECT_EQ(text.find('\r'), std::string::npos);
|
||||
const auto fields = SplitTabs(text.substr(0, text.size() - 1));
|
||||
const std::vector<std::string> base{"record", "schema", "policy_id", "step", "increment", "time", "iteration", "stage", "outcome", "failure_code", "field_pass", "accepted"};
|
||||
for (std::size_t i = 0; i < base.size(); ++i)
|
||||
EXPECT_EQ(fields[i].substr(0, fields[i].find('=')), base[i]);
|
||||
const std::vector<std::string> suffixes{
|
||||
"residual_numerator", "residual_scale", "spatial_flux", "time_average_flux",
|
||||
"correction_numerator", "increment_norm", "estimated_correction",
|
||||
"residual_threshold", "correction_threshold", "residual_unit", "correction_unit",
|
||||
"branch", "residual_pass", "correction_pass", "field_pass"};
|
||||
for (std::size_t c = 0; c < 2; ++c)
|
||||
for (std::size_t i = 0; i < suffixes.size(); ++i)
|
||||
EXPECT_EQ(fields[base.size() + c * suffixes.size() + i].substr(
|
||||
0, fields[base.size() + c * suffixes.size() + i].find('=')),
|
||||
(c == 0 ? "force_" : "moment_") + suffixes[i]);
|
||||
EXPECT_EQ(FieldValue(fields, "step"), "step=\xED\x95\x9C\xEA\xB8\x80=Step\\\\tail\\tline\\nend\\r");
|
||||
EXPECT_EQ(FieldValue(fields, "force_correction_numerator"), "force_correction_numerator=not-evaluated");
|
||||
EXPECT_EQ(FieldValue(fields, "force_increment_norm"), "force_increment_norm=0");
|
||||
EXPECT_EQ(FieldValue(fields, "strict_residual_pass"), "strict_residual_pass=not-evaluated");
|
||||
EXPECT_EQ(FieldValue(fields, "moment_correction_unit"), "moment_correction_unit=rad");
|
||||
EXPECT_DOUBLE_EQ(std::stod(ValueAfterEquals(FieldValue(fields, "force_residual_numerator"))),
|
||||
*record.fields[0].residual_numerator);
|
||||
}
|
||||
|
||||
TEST(DynamicFieldIterationLog, InvalidFieldEvidenceFailsWithoutWritingPartialRecord) {
|
||||
TempDirectory directory{"field-v2-invalid"};
|
||||
const auto path = directory.Path() / "log.txt";
|
||||
for (int kind = 0; kind < 4; ++kind) {
|
||||
SCOPED_TRACE(kind);
|
||||
fesa::NewtonIterationLogWriter writer;
|
||||
ASSERT_TRUE(writer.Open(path).IsOk());
|
||||
auto record = CompleteDynamicRecord();
|
||||
record.policy_id = "abaqus2025-field-on-fesa-v1";
|
||||
ASSERT_TRUE(writer.Write(record).IsOk());
|
||||
ASSERT_TRUE(writer.Flush().IsOk());
|
||||
const auto prefix = ReadLogBytes(path);
|
||||
if (kind == 0) record.fields[0].branch = "unknown";
|
||||
if (kind == 1) record.fields[0].estimated_correction = std::numeric_limits<double>::infinity();
|
||||
if (kind == 2) record.fields[1].residual_scale = -1;
|
||||
if (kind == 3) record.policy_id = "unknown";
|
||||
ExpectLogOutputFailure(writer.Write(record));
|
||||
ExpectLogOutputFailure(writer.Flush());
|
||||
EXPECT_EQ(ReadLogBytes(path), prefix);
|
||||
}
|
||||
}
|
||||
|
||||
// Independent strict consumer: production has a writer, no public log reader.
|
||||
bool ReadFieldV2(const std::string& bytes) {
|
||||
if (bytes.empty() || bytes.back() != '\n' || bytes.find('\r') != std::string::npos ||
|
||||
std::count(bytes.begin(), bytes.end(), '\n') != 1) return false;
|
||||
std::map<std::string, std::string> values;
|
||||
for (const auto& field : SplitTabs(bytes.substr(0, bytes.size() - 1))) {
|
||||
const auto equal = field.find('=');
|
||||
if (equal == std::string::npos || !values.emplace(field.substr(0, equal), field.substr(equal + 1)).second)
|
||||
return false;
|
||||
}
|
||||
for (const auto* key : {"record", "schema", "policy_id", "step", "increment", "time", "iteration", "stage", "outcome", "failure_code", "field_pass", "accepted"})
|
||||
if (!values.count(key)) return false;
|
||||
if (values["record"] != "dynamic-newton-iteration" || values["schema"] != "hht-log-v2" ||
|
||||
values["policy_id"] != "abaqus2025-field-on-fesa-v1") return false;
|
||||
const auto numeric = [](const std::string& text, bool missing) {
|
||||
if (missing && text == "not-evaluated") return true;
|
||||
double value = 0;
|
||||
const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
return parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size() && std::isfinite(value) && value >= 0;
|
||||
};
|
||||
const auto boolean = [](const std::string& text, bool missing) {
|
||||
return text == "0" || text == "1" || (missing && text == "not-evaluated");
|
||||
};
|
||||
for (const auto* key : {"increment", "iteration"}) {
|
||||
std::uint64_t value = 0;
|
||||
const auto& text = values[key];
|
||||
const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size()) return false;
|
||||
}
|
||||
if (!numeric(values["time"], false) || !boolean(values["field_pass"], true) || !boolean(values["accepted"], false)) return false;
|
||||
for (const auto* prefix : {"force_", "moment_"}) {
|
||||
for (const auto* suffix : {"residual_numerator", "residual_scale", "spatial_flux", "time_average_flux", "correction_numerator", "increment_norm", "estimated_correction", "residual_threshold", "correction_threshold"}) {
|
||||
const auto found = values.find(std::string(prefix) + suffix);
|
||||
if (found == values.end() || !numeric(found->second, true)) return false;
|
||||
}
|
||||
for (const auto* suffix : {"residual_pass", "correction_pass", "field_pass"}) {
|
||||
const auto found = values.find(std::string(prefix) + suffix);
|
||||
if (found == values.end() || !boolean(found->second, true)) return false;
|
||||
}
|
||||
const auto branch = values.find(std::string(prefix) + "branch");
|
||||
if (branch == values.end() || (branch->second != "normal" && branch->second != "zero-flux" &&
|
||||
branch->second != "linear" && branch->second != "nonquadratic" && branch->second != "not-evaluated")) return false;
|
||||
if (values[std::string(prefix) + "residual_unit"] != (std::string(prefix) == "force_" ? "force" : "moment") ||
|
||||
values[std::string(prefix) + "correction_unit"] != (std::string(prefix) == "force_" ? "length" : "rad")) return false;
|
||||
}
|
||||
for (const auto& entry : values)
|
||||
for (std::size_t i = 0; i < entry.second.size(); ++i)
|
||||
if (entry.second[i] == '\\') {
|
||||
if (++i == entry.second.size() || (entry.second[i] != '\\' && entry.second[i] != 't' && entry.second[i] != 'n' && entry.second[i] != 'r')) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(DynamicFieldIterationLog, IndependentReaderRejectsMalformedVersionedRecords) {
|
||||
TempDirectory directory{"field-reader"};
|
||||
const auto path = directory.Path() / "log.txt";
|
||||
fesa::NewtonIterationLogWriter writer;
|
||||
ASSERT_TRUE(writer.Open(path).IsOk());
|
||||
auto record = CompleteDynamicRecord();
|
||||
record.policy_id = "abaqus2025-field-on-fesa-v1";
|
||||
ASSERT_TRUE(writer.Write(record).IsOk());
|
||||
ASSERT_TRUE(writer.Close().IsOk());
|
||||
const auto valid = ReadLogBytes(path);
|
||||
ASSERT_TRUE(ReadFieldV2(valid));
|
||||
const auto replaced = [&](const std::string& from, const std::string& to) {
|
||||
auto text = valid;
|
||||
const auto start = text.find(from);
|
||||
EXPECT_NE(start, std::string::npos);
|
||||
text.replace(start, from.size(), to);
|
||||
return text;
|
||||
};
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("schema=hht-log-v2", "schema=hht-log-v1")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\tincrement=2", "\tincrement=-1")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\ttime=0.25", "\ttime=nan")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\ttime=0.25", "\ttime=inf")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("force_residual_numerator=not-evaluated", "force_residual_numerator=1junk")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("force_branch=not-evaluated", "force_branch=unknown")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\tfield_pass=not-evaluated", "\tfield_pass=true")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\tstep=", "\tstep=bad\\x")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\taccepted=0", "\taccepted=0\taccepted=0")));
|
||||
EXPECT_FALSE(ReadFieldV2(replaced("\tforce_increment_norm=not-evaluated", "")));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user