From c3b671d2679d947b3eb80b20f91cafc3ea60822c Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Tue, 1 Sep 2026 13:11:27 +0900 Subject: [PATCH] feat(shell-surface-pressure-load): step 2 - reject-invalid-pressure-input --- src/fesa/io/abaqus/domain_mapping_context.cpp | 144 ++++++++- .../shell_surface_pressure_domain_test.cpp | 284 ++++++++++++++++++ 2 files changed, 412 insertions(+), 16 deletions(-) diff --git a/src/fesa/io/abaqus/domain_mapping_context.cpp b/src/fesa/io/abaqus/domain_mapping_context.cpp index 0544ed2..37f9071 100644 --- a/src/fesa/io/abaqus/domain_mapping_context.cpp +++ b/src/fesa/io/abaqus/domain_mapping_context.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -313,7 +314,9 @@ class DomainMappingContext::Impl { } bool ValidateParameters(const KeywordBlock& block, - const std::vector& allowed) { + const std::vector& allowed, + std::string_view unsupported_code = + "invalid-keyword-parameter") { std::set seen; for (const auto& candidate : block.parameters) { if (!seen.insert(candidate.name).second) { @@ -323,10 +326,10 @@ class DomainMappingContext::Impl { } if (std::find(allowed.begin(), allowed.end(), candidate.name) == allowed.end()) { - return InputFailure( - "invalid-keyword-parameter", block.location, block.canonical_name, - candidate.name, - "The keyword parameter is outside the approved subset."); + return InputFailure(std::string{unsupported_code}, block.location, + block.canonical_name, candidate.name, + "The keyword parameter is outside the approved " + "subset."); } } return true; @@ -437,7 +440,17 @@ class DomainMappingContext::Impl { return InputFailure("invalid-numeric-value", location, keyword, text, "Pressure values require C-locale decimal syntax."); } - return ParseDouble(text, value, location, keyword); + const char* const begin = text.data(); + const char* const end = begin + text.size(); + const char* const numeric_begin = *begin == '+' ? begin + 1U : begin; + const auto parsed = std::from_chars(numeric_begin, end, value, + std::chars_format::general); + if (parsed.ec != std::errc{} || parsed.ptr != end || + !std::isfinite(value)) { + return InputFailure("invalid-numeric-value", location, keyword, text, + "Pressure values must be finite C-locale values."); + } + return true; } bool ParseNonlinearStaticDouble(const std::string& text, double& value, @@ -532,6 +545,22 @@ class DomainMappingContext::Impl { }); } + bool IsPressureNameToken(const std::string_view text) const noexcept { + if (text.empty() || text.size() > 80U) { + return false; + } + const auto is_initial = [](const char value) { + return (value >= 'A' && value <= 'Z') || + (value >= 'a' && value <= 'z') || value == '_'; + }; + const auto is_subsequent = [is_initial](const char value) { + return is_initial(value) || (value >= '0' && value <= '9') || + value == '-' || value == '_'; + }; + return is_initial(text.front()) && + std::all_of(text.begin() + 1, text.end(), is_subsequent); + } + void ParseBlocks() { definition_.source_path = input_.source_path; definition_.source_content_identity = input_.source_content_identity; @@ -800,6 +829,10 @@ class DomainMappingContext::Impl { return; } AddIgnoredWarning(block); + } else if (block.canonical_name == "SURFACE") { + InputFailure("unsupported-surface-scope", block.location, + block.canonical_name, "", + "Pressure surfaces are supported only at assembly scope."); } else { pending_section_.reset(); beam_section_context_active_ = false; @@ -1319,6 +1352,12 @@ class DomainMappingContext::Impl { "An assembly cannot be nested in an instance."); return; } + if (block.canonical_name == "SURFACE") { + InputFailure("unsupported-surface-scope", block.location, + block.canonical_name, instances_.back().name, + "Pressure surfaces are unsupported inside an instance."); + return; + } InputFailure("unsupported-instance-mesh-semantics", block.location, block.canonical_name, instances_.back().name, "Instance-local mesh definitions are unsupported."); @@ -1334,10 +1373,19 @@ class DomainMappingContext::Impl { return; } const auto* name = RequiredParameterValue(block, name_parameter); - const auto* instance = RequiredParameterValue(block, "INSTANCE"); - if (name == nullptr || instance == nullptr) { + if (name == nullptr) { return; } + const auto* instance_parameter = Parameter(block, "INSTANCE"); + if (instance_parameter == nullptr || !instance_parameter->value || + instance_parameter->value->empty()) { + InputFailure(node_set ? "invalid-keyword-parameter" + : "implicit-instance-elset", + block.location, block.canonical_name, "INSTANCE", + "Assembly element sets require an explicit INSTANCE."); + return; + } + const std::string& instance = *instance_parameter->value; const auto* generate_parameter = Parameter(block, "GENERATE"); if (generate_parameter != nullptr && generate_parameter->value) { InputFailure("invalid-keyword-parameter", block.location, @@ -1359,7 +1407,30 @@ class DomainMappingContext::Impl { "element-set namespace."); return; } - RawAssemblySet set{node_set, *name, *instance, {}, block.location}; + if (!node_set && generate_parameter == nullptr) { + for (const auto& row : block.data) { + for (const auto& field : WithoutTrailingEmpty(row.fields)) { + if (field.empty()) { + continue; + } + const bool numeric = !field.empty() && + std::all_of(field.begin(), field.end(), + [](const char value) { + return value >= '0' && + value <= '9'; + }); + if (!numeric) { + const std::string code = AsciiCaseInsensitiveEquals(field, *name) + ? "cyclic-elset" + : "nested-elset"; + InputFailure(code, row.location, block.canonical_name, field, + "Assembly ELSET members must be direct labels."); + return; + } + } + } + } + RawAssemblySet set{node_set, *name, instance, {}, block.location}; if (!ParseSetMembers(block, generate_parameter != nullptr, set.members)) { return; } @@ -1367,7 +1438,8 @@ class DomainMappingContext::Impl { } void ParseElementSurface(const KeywordBlock& block) { - if (!ValidateParameters(block, {"TYPE", "NAME"})) { + if (!ValidateParameters(block, {"TYPE", "NAME"}, + "unsupported-parameter")) { return; } const auto* type = RequiredParameterValue(block, "TYPE"); @@ -1375,6 +1447,12 @@ class DomainMappingContext::Impl { if (type == nullptr || name == nullptr) { return; } + if (!IsPressureNameToken(*name)) { + InputFailure("invalid-surface-name", block.location, + block.canonical_name, *name, + "Surface names must use the approved ASCII name syntax."); + return; + } if (!AsciiCaseInsensitiveEquals(*type, "ELEMENT")) { InputFailure("unsupported-surface-type", block.location, block.canonical_name, *type, @@ -1387,7 +1465,7 @@ class DomainMappingContext::Impl { return; } if (block.data.empty()) { - InputFailure("invalid-data-arity", block.location, block.canonical_name, + InputFailure("empty-surface", block.location, block.canonical_name, *name, "An element surface requires at least one member."); return; } @@ -1563,7 +1641,8 @@ class DomainMappingContext::Impl { } void ParseDsload(const KeywordBlock& block) { - if (!ValidateParameters(block, {}) || block.data.empty()) { + if (!ValidateParameters(block, {}, "unsupported-dsload-parameter") || + block.data.empty()) { if (!failure_) { InputFailure("invalid-data-arity", block.location, block.canonical_name, "", "DSLOAD requires one or more rows."); @@ -1578,6 +1657,13 @@ class DomainMappingContext::Impl { "", "DSLOAD rows require surface, P, magnitude."); return; } + if (!IsPressureNameToken(row.fields[0])) { + InputFailure("invalid-surface-name", row.location, + block.canonical_name, row.fields[0], + "DSLOAD surface names must use the approved ASCII name " + "syntax."); + return; + } if (!AsciiCaseInsensitiveEquals(row.fields[1], "P")) { InputFailure("unsupported-load-type", row.location, block.canonical_name, row.fields[1], @@ -2469,6 +2555,12 @@ class DomainMappingContext::Impl { "A direct surface member must be instance.positive-element-label."); } const std::string instance_name = member.target.substr(0U, dot); + if (!IsPressureNameToken(instance_name)) { + return InputFailure( + "unsupported-surface-target", member.location, "SURFACE", + member.target, + "A direct surface member must use an identity instance name."); + } std::int64_t source_label = 0; if (!ParseInteger(member.target.substr(dot + 1U), source_label, member.location, "SURFACE", true)) { @@ -2479,6 +2571,12 @@ class DomainMappingContext::Impl { return AsciiCaseInsensitiveEquals(value.name, instance_name); }); if (raw_instance == instances_.end()) { + if (FindPart(instance_name) != nullptr) { + return InputFailure( + "unsupported-surface-target", member.location, "SURFACE", + member.target, + "A direct surface member cannot use a part name as an instance."); + } return InputFailure("unresolved-reference", member.location, "SURFACE", member.target, "A direct surface member instance must resolve."); @@ -2507,15 +2605,29 @@ class DomainMappingContext::Impl { const RawSurfaceMember& member, std::vector>* candidates) { + if (!IsPressureNameToken(member.target)) { + return InputFailure("unsupported-surface-target", member.location, + "SURFACE", member.target, + "A surface ELSET must use the approved name syntax."); + } const auto raw_set = std::find_if( assembly_sets_.begin(), assembly_sets_.end(), [&member](const RawAssemblySet& value) { return !value.is_node_set && AsciiCaseInsensitiveEquals(value.name, member.target); }); if (raw_set == assembly_sets_.end()) { - return InputFailure("unresolved-reference", member.location, "SURFACE", - member.target, - "A surface ELSET must be assembly-scoped and explicit-instance."); + const bool part_scoped = std::any_of( + parts_.begin(), parts_.end(), [this, &member](const RawPart& part) { + return FindSet(part.element_sets, member.target) != nullptr; + }); + if (part_scoped) { + return InputFailure( + "cross-scope-target", member.location, "SURFACE", member.target, + "A surface ELSET must be declared at assembly scope."); + } + return InputFailure( + "missing-reference", member.location, "SURFACE", member.target, + "A surface ELSET must be assembly-scoped and explicit-instance."); } const auto raw_instance = std::find_if( instances_.begin(), instances_.end(), [&raw_set](const RawInstance& value) { @@ -2713,7 +2825,7 @@ class DomainMappingContext::Impl { load.surface_name); }); if (surface == definition_.element_surfaces.end()) { - InputFailure("unresolved-reference", load.data_location, "DSLOAD", + InputFailure("missing-reference", load.data_location, "DSLOAD", load.surface_name, "A DSLOAD surface must resolve after surface validation."); return; diff --git a/tests/unit/io/abaqus/shell_surface_pressure_domain_test.cpp b/tests/unit/io/abaqus/shell_surface_pressure_domain_test.cpp index 9d4ed0c..bb6ae73 100644 --- a/tests/unit/io/abaqus/shell_surface_pressure_domain_test.cpp +++ b/tests/unit/io/abaqus/shell_surface_pressure_domain_test.cpp @@ -1,4 +1,5 @@ #include "fesa/analysis/analysis_model.h" +#include "fesa/core/diagnostic.h" #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" @@ -47,6 +48,34 @@ fesa::Result MapText(const std::string& stem, return fesa::AbaqusDomainMapper{}.Map(parsed.Value()); } +fesa::Result ReadText(const std::string& stem, + const std::string& content) { + const TemporaryInputFile input{stem, content}; + return fesa::AbaqusInputReader{}.Read(input.Path()); +} + +std::string ReplaceOnce(std::string text, const std::string& from, + const std::string& to) { + const std::size_t position = text.find(from); + if (position == std::string::npos) { + throw std::runtime_error{"Expected fixture text was not found."}; + } + text.replace(position, from.size(), to); + return text; +} + +void ExpectSingleFailure(const fesa::Result& result, + const std::string& code, std::size_t line, + const std::string& keyword) { + ASSERT_FALSE(result.HasValue()); + ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U); + const fesa::Diagnostic& diagnostic = result.GetStatus().Diagnostics().front(); + EXPECT_EQ(diagnostic.severity, fesa::Severity::kError); + EXPECT_EQ(diagnostic.code, code); + EXPECT_EQ(diagnostic.location.line, line); + EXPECT_EQ(diagnostic.keyword, keyword); +} + std::string ShellDeck(const std::string& assembly_content, const std::string& dsload_rows) { return std::string{R"inp(*Part, name=Shell-Part @@ -302,3 +331,258 @@ TEST(ShellSurfacePressureInput, EXPECT_EQ(loads[1].magnitude_raw, "-1.25E-3"); EXPECT_DOUBLE_EQ(loads[1].magnitude, -0.00125); } + +TEST(ShellSurfacePressureInput, + RejectsNonAsciiOverlengthOrIllegalContinuationAtStableLocation) { + const auto non_ascii = ReadText("non-ascii", "*Heading\n\xFF\n"); + ASSERT_FALSE(non_ascii.HasValue()); + ASSERT_EQ(non_ascii.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(non_ascii.GetStatus().Diagnostics().front().code, + "invalid-input-character"); + EXPECT_EQ(non_ascii.GetStatus().Diagnostics().front().location.line, 2U); + + const auto overlength = ReadText("overlength", std::string(257U, 'x')); + ASSERT_FALSE(overlength.HasValue()); + EXPECT_EQ(overlength.GetStatus().Diagnostics().front().code, + "input-line-too-long"); + EXPECT_EQ(overlength.GetStatus().Diagnostics().front().location.line, 1U); + + const auto continuation = ReadText( + "continuation", + "*Surface, TYPE=ELEMENT, NAME=Pressure-Surface,\n** interruption\n"); + ASSERT_FALSE(continuation.HasValue()); + EXPECT_EQ(continuation.GetStatus().Diagnostics().front().code, + "malformed-keyword"); + EXPECT_EQ(continuation.GetStatus().Diagnostics().front().location.line, 2U); +} + +TEST(ShellSurfacePressureInput, + RejectsUnsupportedSurfaceParametersTypesSidesAndTargetKinds) { + ExpectSingleFailure( + MapText("surface-parameter", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface, " + "TRIM=YES\n" + "Instance-A.10, SPOS\n", + "Pressure-Surface, P, 1.0\n")), + "unsupported-parameter", 27U, "SURFACE"); + ExpectSingleFailure( + MapText("surface-type", + ShellDeck("*SURFACE, TYPE=NODE, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n", + "Pressure-Surface, P, 1.0\n")), + "unsupported-surface-type", 27U, "SURFACE"); + ExpectSingleFailure( + MapText("surface-side", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, S1\n", + "Pressure-Surface, P, 1.0\n")), + "unsupported-surface-side", 28U, "SURFACE"); + ExpectSingleFailure( + MapText("surface-target-kind", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Shell-Part.10, SPOS\n", + "Pressure-Surface, P, 1.0\n")), + "unsupported-surface-target", 28U, "SURFACE"); +} + +TEST(ShellSurfacePressureInput, + RejectsDsloadParametersLabelsDistributionsAndCardinality) { + const std::string parameter_deck = ReplaceOnce( + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n", + "Pressure-Surface, P, 1.0\n"), + "*Dsload", "*Dsload, AMPLITUDE=Ramp"); + ExpectSingleFailure(MapText("dsload-parameter", parameter_deck), + "unsupported-dsload-parameter", 36U, "DSLOAD"); + ExpectSingleFailure( + MapText("dsload-label", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n", + "Pressure-Surface, PNU, 1.0\n")), + "unsupported-load-type", 37U, "DSLOAD"); + ExpectSingleFailure( + MapText("dsload-cardinality", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n", + "Pressure-Surface, P, 1.0, 2.0\n")), + "invalid-data-arity", 37U, "DSLOAD"); +} + +TEST(ShellSurfacePressureInput, + RejectsEmptyMissingAmbiguousAndCrossScopeSurfaceTargets) { + ExpectSingleFailure( + MapText("empty-surface", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n", + "Pressure-Surface, P, 1.0\n")), + "empty-surface", 27U, "SURFACE"); + ExpectSingleFailure( + MapText("missing-surface", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n", + "Missing-Surface, P, 1.0\n")), + "missing-reference", 37U, "DSLOAD"); + ExpectSingleFailure( + MapText("cross-scope-elset", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "S4-Set, SPOS\n", + "Pressure-Surface, P, 1.0\n")), + "cross-scope-target", 28U, "SURFACE"); + ExpectSingleFailure( + MapText("ambiguous-target", + ShellDeck("*ELSET, ELSET=Shared, INSTANCE=Instance-A\n" + "10\n" + "*ELSET, ELSET=Shared, INSTANCE=Instance-B\n" + "10\n" + "*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Shared, SPOS\n", + "Pressure-Surface, P, 1.0\n")), + "duplicate-entity", 29U, "ELSET"); +} + +TEST(ShellSurfacePressureInput, + RejectsImplicitInstanceNestedCyclicAndMalformedElsets) { + ExpectSingleFailure( + MapText("implicit-elset", + ShellDeck("*ELSET, ELSET=Implicit\n" + "10\n" + "*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Implicit, SPOS\n", + "Pressure-Surface, P, 1.0\n")), + "implicit-instance-elset", 27U, "ELSET"); + ExpectSingleFailure( + MapText("nested-elset", + ShellDeck("*ELSET, ELSET=Inner, INSTANCE=Instance-A\n" + "10\n" + "*ELSET, ELSET=Outer, INSTANCE=Instance-A\n" + "Inner\n" + "*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Outer, SPOS\n", + "Pressure-Surface, P, 1.0\n")), + "nested-elset", 30U, "ELSET"); + ExpectSingleFailure( + MapText("cyclic-elset", + ShellDeck("*ELSET, ELSET=Loop, INSTANCE=Instance-A\n" + "Loop\n" + "*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Loop, SPOS\n", + "Pressure-Surface, P, 1.0\n")), + "cyclic-elset", 28U, "ELSET"); + ExpectSingleFailure( + MapText("malformed-elset", + ShellDeck("*ELSET, ELSET=Malformed, INSTANCE=Instance-A\n" + "10, , 20\n" + "*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Malformed, SPOS\n", + "Pressure-Surface, P, 1.0\n")), + "invalid-data-arity", 28U, "ELSET"); +} + +TEST(ShellSurfacePressureInput, + NormalizesExactSameSideDuplicatesWithOneStableWarning) { + const auto result = MapText( + "same-side-duplicate", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n" + "INSTANCE-A.0010, SPOS\n", + "Pressure-Surface, P, 1.0\n")); + + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(OnlySurface(result.Value()).facets.size(), 1U); + ASSERT_EQ(result.Value().Warnings().size(), 1U); + const auto& warning = result.Value().Warnings().front(); + EXPECT_EQ(warning.severity, fesa::Severity::kWarning); + EXPECT_EQ(warning.code, "duplicate-surface-member-normalized"); + EXPECT_EQ(warning.location.line, 29U); + EXPECT_EQ(warning.keyword, "SURFACE"); + EXPECT_EQ(warning.entity_identity, "Pressure-Surface"); +} + +TEST(ShellSurfacePressureInput, RejectsConflictingSidesForOneElement) { + ExpectSingleFailure( + MapText("conflicting-sides", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n" + "Instance-A.10, SNEG\n", + "Pressure-Surface, P, 1.0\n")), + "conflicting-surface-membership", 29U, "SURFACE"); +} + +TEST(ShellSurfacePressureInput, + RejectsNonfiniteDExponentAndLocaleDependentMagnitudes) { + ExpectSingleFailure( + MapText("dsload-nan", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n", + "Pressure-Surface, P, NaN\n")), + "invalid-numeric-value", 37U, "DSLOAD"); + ExpectSingleFailure( + MapText("dsload-d-exponent", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n", + "Pressure-Surface, P, 1.0D+03\n")), + "invalid-numeric-value", 37U, "DSLOAD"); + ExpectSingleFailure( + MapText("dsload-locale", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n", + "Pressure-Surface, P, 1,25\n")), + "invalid-data-arity", 37U, "DSLOAD"); +} + +TEST(ShellSurfacePressureInput, + PreservesAdditiveRepeatedPressureRowsWithoutDeduplication) { + const auto result = MapText( + "repeated-dsload", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n", + "Pressure-Surface, P, 1.0\n" + "pressure-surface, p, 1.0\n")); + + ASSERT_TRUE(result.HasValue()); + const auto& loads = result.Value().Steps()[0U].SurfacePressureLoads(); + ASSERT_EQ(loads.size(), 2U); + EXPECT_EQ(loads[0].load_ordinal, 0U); + EXPECT_EQ(loads[1].load_ordinal, 1U); + EXPECT_EQ(loads[0].data_ordinal, 0U); + EXPECT_EQ(loads[1].data_ordinal, 1U); + EXPECT_DOUBLE_EQ(loads[0].magnitude, loads[1].magnitude); +} + +TEST(ShellSurfacePressureDomain, + MappingFailureCommitsNoPartialSemanticCandidate) { + const auto result = MapText( + "atomic-failure", + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n" + "Instance-A.10, SNEG\n", + "Pressure-Surface, P, 1.0\n")); + + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Diagnostics().front().code, + "conflicting-surface-membership"); +} + +TEST(ShellSurfacePressureInput, + EmitsDiagnosticsInStableSourceOrderAcrossRepeatedRuns) { + const std::string deck = + ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n" + "Instance-A.10, SPOS\n" + "INSTANCE-A.0010, SPOS\n", + "Pressure-Surface, P, 1.0\n"); + const auto first = MapText("stable", deck); + const auto second = MapText("stable", deck); + + ASSERT_TRUE(first.HasValue()); + ASSERT_TRUE(second.HasValue()); + ASSERT_EQ(first.Value().Warnings().size(), 1U); + ASSERT_EQ(second.Value().Warnings().size(), 1U); + const auto& left = first.Value().Warnings().front(); + const auto& right = second.Value().Warnings().front(); + EXPECT_EQ(left.severity, right.severity); + EXPECT_EQ(left.code, right.code); + EXPECT_EQ(left.location.file, right.location.file); + EXPECT_EQ(left.location.line, right.location.line); + EXPECT_EQ(left.keyword, right.keyword); + EXPECT_EQ(left.entity_identity, right.entity_identity); + EXPECT_EQ(left.message, right.message); +}