fix: guard STEP-113 empty annotation and expose PBF blockers
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user