#include #include #include #include #include #include #include #include #include "nonlinear_static_reference_comparison.h" #ifndef FESA_TEST_SOURCE_DIR #error FESA_TEST_SOURCE_DIR must identify the repository root. #endif #ifndef FESA_TEST_BINARY_DIR #error FESA_TEST_BINARY_DIR must identify the CMake binary root. #endif namespace { fesa::test::NonlinearReferenceCase MakePressureCase() { const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; const std::filesystem::path directory = root / "reference" / "shellNL-meshfine-pressure"; return {"shellnl-meshfine-pressure-s4-follower", directory / "shellNL-meshfine-pressure.inp", directory / "shellNL-meshfine-pressure displacements.csv", directory / "shellNL-meshfine-pressure reactions.csv", directory / "shellNL-meshfine-pressure stresses.csv", std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / "shell-surface-pressure-load", fesa::test::NonlinearReferenceArtifactDisposition::kRequired, fesa::test::NonlinearReferenceArtifactDisposition::kRequired, fesa::test::NonlinearReferenceArtifactDisposition::kReviewOnly, fesa::test::NonlinearReferenceArtifactDisposition::kExcluded}; } std::optional FindCachedResults( const fesa::test::NonlinearReferenceCase& reference_case) { const std::filesystem::path solver_library = std::filesystem::path{FESA_TEST_BINARY_DIR} / "src" / "fesa" / "Debug" / "fesa_solver.lib"; std::error_code error; const auto solver_time = std::filesystem::last_write_time(solver_library, error); if (error || !std::filesystem::is_directory(reference_case.execution_directory)) { return std::nullopt; } std::vector candidates; for (const auto& entry : std::filesystem::recursive_directory_iterator( reference_case.execution_directory, error)) { if (error) { return std::nullopt; } if (entry.path().filename() == "shellNL-meshfine-pressure_results.h5" && std::filesystem::is_regular_file(entry.path()) && std::filesystem::last_write_time(entry.path(), error) >= solver_time && !error) { candidates.push_back(entry.path()); } } std::sort(candidates.begin(), candidates.end()); return candidates.empty() ? std::nullopt : std::optional{candidates.back()}; } const fesa::test::NonlinearReferenceComparisonReport& RunPressureComparison() { static const auto report = [] { const auto reference_case = MakePressureCase(); const auto cached = FindCachedResults(reference_case); if (cached.has_value()) { const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(reference_case, *cached); if (comparison.HasValue()) { return comparison.Value(); } } const auto copied = fesa::test::CopyInputToExecutionDirectory(reference_case); if (!copied.HasValue()) { throw std::runtime_error{"Unable to copy pressure reference input."}; } const auto generated = fesa::test::RunFesaApplicationOnCopiedInput( reference_case, copied.Value()); if (!generated.HasValue()) { throw std::runtime_error{"Unable to run pressure reference input."}; } const auto comparison = fesa::test::CompareHdf5ToReferenceCsv( reference_case, generated.Value().generated_results_hdf5); if (!comparison.HasValue()) { throw std::runtime_error{"Unable to compare pressure reference output."}; } return comparison.Value(); }(); return report; } const fesa::test::NonlinearReferenceFamilyReport& FindFamily( const fesa::test::NonlinearReferenceComparisonReport& report, const std::string& identity) { const auto found = std::find_if( report.families.begin(), report.families.end(), [&identity](const fesa::test::NonlinearReferenceFamilyReport& family) { return family.family_identity == identity; }); if (found == report.families.end()) { throw std::runtime_error{"Required comparison family is absent."}; } return *found; } fesa::test::ReferenceToleranceFamilyDecision MakeTolerance(const bool passed) { fesa::test::ReferenceToleranceFamilyDecision result{}; result.passed = passed; return result; } } // namespace TEST(ShellSurfacePressureReference, AuditsExactRequiredArtifactsAndIgnoresOptionalReviewArtifacts) { const auto artifacts = fesa::test::ArtifactCheck(MakePressureCase()); ASSERT_TRUE(artifacts.HasValue()); EXPECT_EQ(MakePressureCase().reaction_artifact_disposition, fesa::test::NonlinearReferenceArtifactDisposition::kReviewOnly); EXPECT_EQ(MakePressureCase().stress_artifact_disposition, fesa::test::NonlinearReferenceArtifactDisposition::kExcluded); ASSERT_EQ(artifacts.Value().size(), 2U); EXPECT_EQ(artifacts.Value()[0U].byte_size, 56665U); EXPECT_EQ(artifacts.Value()[0U].sha256, "53718CCC9B41C23FCC1B509A51D66F1347E87ACA43A74F1D6CEC4D2085F4B90E"); EXPECT_EQ(artifacts.Value()[1U].byte_size, 111182U); EXPECT_EQ(artifacts.Value()[1U].sha256, "2FE53F98EAC9F04779100721C3EDF8049EB1DBFE733EBCBD819AEEED03B81198"); } TEST(ShellSurfacePressureReference, SelectsUniqueSchemaOneFactorOneAcceptedFrame) { const auto& report = RunPressureComparison(); EXPECT_TRUE(report.blocking_passed); EXPECT_EQ(report.case_id, "shellnl-meshfine-pressure-s4-follower"); } TEST(ShellSurfacePressureReference, Projects729WideRowsTo2187LogicalRowsPerFamily) { const auto& report = RunPressureComparison(); EXPECT_EQ(FindFamily(report, "pressure_nodal_translation_blocking").row_count, 2187U); EXPECT_EQ(FindFamily(report, "pressure_nodal_rotation_warning").row_count, 2187U); } TEST(ShellSurfacePressureReference, MatchesByInstanceNodeLabelAndComponentNotRowOrder) { const auto& report = RunPressureComparison(); const auto& translation = FindFamily(report, "pressure_nodal_translation_blocking"); EXPECT_EQ(translation.components, (std::vector{"U1", "U2", "U3"})); EXPECT_EQ(translation.tolerance.rows.size(), 2187U); } TEST( ShellSurfacePressureReference, RejectsSchemaIdentityCardinalityMissingExtraDuplicateAndNonfiniteBeforeTolerance) { const auto& report = RunPressureComparison(); const std::filesystem::path invalid_hdf5 = report.generated_results_hdf5.parent_path() / "invalid-pressure.h5"; { std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc}; stream << "not an HDF5 file"; } EXPECT_FALSE( fesa::test::CompareHdf5ToReferenceCsv(MakePressureCase(), invalid_hdf5) .HasValue()); fesa::test::NonlinearReferenceComparisonReport precheck_report{}; precheck_report.blocking_passed = true; precheck_report.precheck_findings = { {"pressure_nodal_translation_blocking", fesa::test::NonlinearReferenceDisposition::kBlocking, "missing-extra-duplicate-or-nonfinite", {}}}; EXPECT_EQ(fesa::test::ClassifyReferenceComparison(precheck_report), fesa::test::NonlinearReferenceClassification::kFailed); } TEST(ShellSurfacePressureReference, ClassifiesTranslationBlockingAndRotationWarningOnly) { fesa::test::NonlinearReferenceComparisonReport report{}; report.blocking_passed = true; report.families = {{"pressure_nodal_translation_blocking", {"U1", "U2", "U3"}, fesa::test::NonlinearReferenceDisposition::kBlocking, 2187U, MakeTolerance(true)}, {"pressure_nodal_rotation_warning", {"UR1", "UR2", "UR3"}, fesa::test::NonlinearReferenceDisposition::kWarningOnly, 2187U, MakeTolerance(false)}}; EXPECT_EQ( fesa::test::ClassifyReferenceComparison(report), fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences); } TEST(ShellSurfacePressureReference, ExcludesReactionAndStressFamiliesFromVerdict) { const auto& report = RunPressureComparison(); EXPECT_EQ(report.families.size(), 2U); EXPECT_EQ( FindFamily(report, "pressure_nodal_translation_blocking").disposition, fesa::test::NonlinearReferenceDisposition::kBlocking); EXPECT_EQ(FindFamily(report, "pressure_nodal_rotation_warning").disposition, fesa::test::NonlinearReferenceDisposition::kWarningOnly); } TEST(ShellSurfacePressureReference, UsesUnchangedCommonScaleNearZeroRelativeAndRmsPolicy) { const auto& translation = FindFamily(RunPressureComparison(), "pressure_nodal_translation_blocking"); EXPECT_EQ(translation.tolerance.near_zero_band, fesa::test::ReferenceTolerancePolicy::kNearZeroRatio * translation.tolerance.reference_scale); EXPECT_LE(translation.tolerance.relative_rms, fesa::test::ReferenceTolerancePolicy::kRelativeRmsTolerance); EXPECT_TRUE(translation.tolerance.rms_passed); } TEST(ShellSurfacePressureReference, WritesDeterministicComparisonReport) { const auto& first = RunPressureComparison(); const auto comparison = fesa::test::CompareHdf5ToReferenceCsv( MakePressureCase(), first.generated_results_hdf5); ASSERT_TRUE(comparison.HasValue()); ASSERT_EQ(first.families.size(), comparison.Value().families.size()); for (std::size_t index = 0U; index < first.families.size(); ++index) { const auto& expected = first.families[index]; const auto& actual = comparison.Value().families[index]; EXPECT_EQ(actual.family_identity, expected.family_identity); EXPECT_EQ(actual.components, expected.components); EXPECT_EQ(actual.disposition, expected.disposition); EXPECT_EQ(actual.row_count, expected.row_count); EXPECT_DOUBLE_EQ(actual.tolerance.reference_scale, expected.tolerance.reference_scale); EXPECT_DOUBLE_EQ(actual.tolerance.relative_rms, expected.tolerance.relative_rms); EXPECT_EQ(actual.tolerance.worst_row, expected.tolerance.worst_row); } }