fix: guard STEP-113 empty annotation and expose PBF blockers
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
tei
2026-10-11 11:50:03 +09:00
parent e2d202b560
commit c48a72a9d4
14 changed files with 726 additions and 12 deletions

View File

@@ -4,8 +4,10 @@
#include <mln/test/map_adapter.hpp>
#include <mln/annotation/annotation.hpp>
#include <mln/annotation/annotation_manager.hpp>
#include <mln/style/style.hpp>
#include <mln/style/image.hpp>
#include <mln/style/layers/symbol_layer.hpp>
#include <mln/map/map_options.hpp>
#include <mln/util/io.hpp>
#include <mln/util/run_loop.hpp>
@@ -66,6 +68,53 @@ TEST(Annotations, SymbolAnnotation) {
// }
}
TEST(Annotations, Step116EmptyPointLayerRestoredBeforeRealAdd) {
AnnotationTest test;
test.map.getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json"));
test.frontend.render(test.map);
auto& style = test.map.getStyle();
ASSERT_NE(style.getSource(AnnotationManager::SourceID), nullptr);
ASSERT_TRUE(style.removeLayer(AnnotationManager::PointLayerID));
test.map.addAnnotationImage(namedMarker("default_marker"));
test.map.addAnnotation(SymbolAnnotation{Point<double>(0, 0), "default_marker"});
// Check before rendering: a polling/render callback is too late to repair
// the immutable UpdateParameters captured by Map::onUpdate.
ASSERT_NE(style.getLayer(AnnotationManager::PointLayerID), nullptr);
test.checkRendering("point_annotation");
}
TEST(Annotations, Step116NoOpUpdateRepairsPointLayerAndSchedulesRender) {
AnnotationTest test;
test.map.getStyle().loadJSON(util::read_file("test/fixtures/api/empty.json"));
test.map.addAnnotationImage(namedMarker("default_marker"));
const SymbolAnnotation annotation{Point<double>(0, 0), "default_marker"};
const auto id = test.map.addAnnotation(annotation);
test.checkRendering("point_annotation");
ASSERT_TRUE(test.map.getStyle().removeLayer(AnnotationManager::PointLayerID));
test.map.updateAnnotation(id, annotation);
ASSERT_NE(test.map.getStyle().getLayer(AnnotationManager::PointLayerID), nullptr);
test.checkRendering("point_annotation");
}
TEST(Annotations, Step116ReloadRestoresHelperWithoutReplacingExistingLayer) {
AnnotationTest test;
const auto json = util::read_file("test/fixtures/api/empty.json");
test.map.getStyle().loadJSON(json);
test.frontend.render(test.map);
auto& style = test.map.getStyle();
ASSERT_TRUE(style.removeLayer(AnnotationManager::PointLayerID));
auto supplied = std::make_unique<mln::style::SymbolLayer>(AnnotationManager::PointLayerID,
AnnotationManager::SourceID);
auto* identity = supplied.get();
style.addLayer(std::move(supplied));
test.map.addAnnotationImage(namedMarker("default_marker"));
test.map.addAnnotation(SymbolAnnotation{Point<double>(0, 0), "default_marker"});
EXPECT_EQ(style.getLayer(AnnotationManager::PointLayerID), identity);
style.loadJSON(json);
test.checkRendering("point_annotation");
EXPECT_NE(style.getLayer(AnnotationManager::PointLayerID), nullptr);
}
TEST(Annotations, SymbolAnnotationTileBoundary) {
// Almost exactly the same as SymbolAnnotation test above, but offset my
// fractions of a degree tests precision issue from

View File

@@ -1200,6 +1200,111 @@ TEST(RenderSegmentLocality, ReadinessAndFadeFollowOnlyDependencies) {
EXPECT_FALSE(assessRenderSegmentSources({{}, {}, true}, lookup(true, false)).ready);
}
TEST(Step116Safety, MissingSourceIsNotAnEmptyDependencyEvenWithRevisionTombstone) {
const RenderSegmentDependencies dependencies{{"chart", "tide"}, {"symbol/glyph/image/placement"}, false};
RenderSegmentRevisionBook book;
book.advanceSource("chart");
EXPECT_FALSE(book.revisionFor(dependencies));
bool exists = false;
bool loaded = false;
const auto lookup = [&](const std::string& id) -> std::optional<RenderSegmentSourceStatus> {
if (id == "chart") return RenderSegmentSourceStatus{true, false};
if (id == "tide" && exists) return RenderSegmentSourceStatus{loaded, false};
return std::nullopt;
};
auto assessment = assessRenderSegmentSources(dependencies, lookup);
EXPECT_FALSE(assessment.ready);
EXPECT_STREQ(assessment.reason, "missing-source");
exists = true;
book.advanceSource("tide"); // source addition, before async tile delivery
const auto created = book.revisionFor(dependencies);
ASSERT_TRUE(created);
EXPECT_FALSE(assessRenderSegmentSources(dependencies, lookup).ready);
loaded = true;
book.advanceSource("tide"); // async layout/tile mutation
EXPECT_NE(book.revisionFor(dependencies), created);
EXPECT_TRUE(assessRenderSegmentSources(dependencies, lookup).stable);
exists = false;
book.advanceSource("tide"); // tombstone survives removal; readiness must not
ASSERT_TRUE(book.revisionFor(dependencies));
EXPECT_FALSE(assessRenderSegmentSources(dependencies, lookup).ready);
exists = true;
book.advanceSource("tide"); // same-name restoration receives a new revision
EXPECT_NE(book.revisionFor(dependencies), created);
}
TEST(Step116Safety, MissingSourceAuditDoesNotHidePlacementOrMutateSnapshot) {
RenderSegmentDiagnostics snapshot;
RenderSegmentDiagnosticsEntry segment;
segment.strategy = RenderSegmentStrategy::Retained;
segment.dependentSourceIds = {"navsea_delivery", "tide"};
segment.placementCapable = true;
segment.placementGatePassed = false;
segment.cache.emplace();
segment.cache->lastFallbackReason = "missing-source";
snapshot.segments.push_back(segment);
RenderSourceReadinessDiagnostics source;
source.sourceId = "navsea_delivery";
source.sourceType = "vector";
snapshot.sources.push_back(source);
auto json = renderSegmentDiagnosticsToJSON(snapshot);
EXPECT_NE(json.find("\"missingSourceIds\":[\"tide\"]"), std::string::npos);
EXPECT_NE(json.find("\"placementGatePassed\":false"), std::string::npos);
EXPECT_EQ(renderSegmentDiagnosticsToJSON(snapshot), json);
source.sourceId = "tide";
snapshot.sources.push_back(source);
EXPECT_NE(renderSegmentDiagnosticsToJSON(snapshot).find("\"missingSourceIds\":[]"), std::string::npos);
// Presence alone grants neither a successful Build nor a placement pass.
EXPECT_EQ(snapshot.segments[0].cache->build, 0u);
EXPECT_FALSE(snapshot.segments[0].placementGatePassed);
}
TEST(Step116Safety, SymbolContinuousBranchesAndResourceChangesRemainProtected) {
const std::vector<StyleLayerDiagnostics> layers{
{0, "tide-spots", "symbol", false, "tide", std::nullopt}};
ASSERT_TRUE(renderSegmentHasPlacementCapableLayer(layers, 0, 1));
// Normal Full: recompute => changed; recent => stale/transition.
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 0.0f, false));
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, false, 0.0f, true));
EXPECT_FALSE(renderSegmentPlacementGatePassed(true, true, 1.0f, false)); // transitions disabled
const auto dependencies = renderSegmentDependencies(layers, 0, 1);
RenderSegmentRevisionBook book;
book.advanceSource("tide");
const auto unchanged = book.revisionFor(dependencies);
EXPECT_EQ(book.revisionFor(dependencies), unchanged);
// Test the revision contract, not fabricated glyph/tile events or GPU completion.
for (const auto* event : {"style", "glyph", "image", "context/reload"}) {
SCOPED_TRACE(event);
const auto before = book.revisionFor(dependencies);
book.advanceGlobal();
EXPECT_NE(book.revisionFor(dependencies), before);
}
for (const auto* event : {"source-update", "tile/layout"}) {
SCOPED_TRACE(event);
const auto before = book.revisionFor(dependencies);
book.advanceSource("tide");
EXPECT_NE(book.revisionFor(dependencies), before);
}
}
TEST(Step116Safety, AnnotationMutationAndLayerOrderDisableReloadKeepAllShadowFailures) {
auto input = fastPathInput();
input.cacheValid = false;
input.annotationMutation = true;
input.unsupportedContinuousLayer = true;
input.supportedScene = false;
input.topologyMatches = false; // added/reordered layer
input.retainedEnabled = false; // explicit disable
input.hasPreparedTree = false; // style reload/context replacement
const auto decision = evaluateParticleFastPath(input);
EXPECT_FALSE(decision.candidate);
EXPECT_EQ(decision.reason, ParticleFastPathMissReason::NoPreparedTree);
const auto failures = particleFastGateFailureMask(input);
for (const auto bit : {0, 1, 2, 10, 21, 22, 23}) EXPECT_NE(failures & (1u << bit), 0u);
auto unmodified = fastPathInput();
EXPECT_EQ(particleFastGateFailureMask(unmodified), 0u); // predicate-only; no rendered Fast claim
}
TEST(RenderSegmentLocality, MixedExecutionDrawsEachLiveRangeOnceInStyleOrder) {
ResolvedRenderSegmentPlan plan;
plan.valid = true;