#include #include #include #include #include #include using namespace mbgl; namespace { particle::DecodedFieldPlane plane(uint16_t semantic, particle::FieldValueClass valueClass, uint16_t unit, uint16_t group, particle::FieldComponent component, particle::FieldDType dtype, bool required, std::shared_ptr> storage, std::string identity = "plane") { return {.key = {semantic, group, component}, .dtype = dtype, .valueClass = valueClass, .unitID = unit, .requiredFlag = required, .count = 4, .scale = 0.1f, .offset = 0.0f, .contentIdentity = std::move(identity), .storage = std::move(storage)}; } particle::DecodedFieldFrame frame() { const auto numbers = std::make_shared>(std::vector{1, 2, 3, 4}); const auto bits = std::make_shared>(std::vector{0x0F}); particle::DecodedFieldFrame value; value.frameContentIdentity = {"future-product", 1, 1, 1'000}; value.referenceTimeMs = 1'000; value.grid = {.layout = 1, .projection = 1, .sampleOrder = 1, .gridRegistration = 1, .width = 2, .height = 2, .west = 130.0, .south = 30.0, .east = 131.0, .north = 31.0}; value.validityContentIdentity = "validity"; value.validityBits = bits; value.validityPlane = plane(900, particle::FieldValueClass::Validity, particle::FieldUnitNone, 0, particle::FieldComponent::None, particle::FieldDType::Bitset, true, bits, "validity"); value.planes.push_back(plane(777, particle::FieldValueClass::Scalar, 65432, 0, particle::FieldComponent::None, particle::FieldDType::UInt8, true, numbers, "scalar")); return value; } particle::ResolvedFieldProfileContract contract() { particle::ResolvedFieldProfileContract value; value.profileID = "future.product-42"; value.profileVersion = 1; value.allowAdditionalPlanes = true; value.requiredPlanes = {{777, particle::FieldValueClass::Scalar, 65432, 0, particle::FieldComponent::None, true}, {900, particle::FieldValueClass::Validity, particle::FieldUnitNone, 0, particle::FieldComponent::None, true}}; return value; } particle::FieldProfileValidationCode validate(const particle::ResolvedFieldProfileContract& c, const particle::DecodedFieldFrame& f) { return particle::FieldProfileValidator::validate(c, f).code; } std::shared_ptr runtimeFrame(uint64_t time, uint64_t generation, uint64_t version, std::string content, std::string id = "field") { auto value = frame(); value.frameContentIdentity = {std::move(id), generation, version, time}; value.planes.front().contentIdentity = std::move(content); return std::make_shared(std::move(value)); } } // namespace TEST(ParticleFieldProfileValidator, AcceptsGenericProfileAndOpaqueScalarUnit) { EXPECT_EQ(validate(contract(), frame()), particle::FieldProfileValidationCode::Accepted); } TEST(ParticleFieldProfileValidator, EnforcesRequiredPlanesAndAdditionalPlanePolicy) { auto c = contract(); auto f = frame(); f.planes.clear(); EXPECT_TRUE(f.isStructurallyValid()); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::MissingRequiredPlane); f = frame(); f.planes.front().unitID = 99; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::PlaneDescriptorMismatch); f = frame(); f.planes.front().requiredFlag = false; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::PlaneDescriptorMismatch); f = frame(); f.planes.push_back(plane(778, particle::FieldValueClass::Scalar, 123, 0, particle::FieldComponent::None, particle::FieldDType::UInt8, true, f.planes.front().storage, "required-extra")); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::UndeclaredRequiredPlane); f.planes.back().requiredFlag = false; c.allowAdditionalPlanes = false; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::AdditionalPlaneNotAllowed); c.allowAdditionalPlanes = true; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::Accepted); } TEST(ParticleFieldProfileValidator, EnforcesSingleSharedValidityStructuralInvariant) { auto f = frame(); const auto numbers = f.planes.front().storage; f.planes.push_back(plane(901, particle::FieldValueClass::Validity, particle::FieldUnitNone, 0, particle::FieldComponent::None, particle::FieldDType::Bitset, false, numbers, "second-validity")); EXPECT_FALSE(f.isStructurallyValid()); EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.requiredFlag = false; EXPECT_FALSE(f.isStructurallyValid()); EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.valueClass = particle::FieldValueClass::Scalar; EXPECT_FALSE(f.isStructurallyValid()); EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.dtype = particle::FieldDType::UInt8; EXPECT_FALSE(f.isStructurallyValid()); EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.unitID = particle::FieldUnitMetersPerSecond; EXPECT_FALSE(f.isStructurallyValid()); EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.key.groupID = 1; EXPECT_FALSE(f.isStructurallyValid()); EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.key.component = particle::FieldComponent::UEast; EXPECT_FALSE(f.isStructurallyValid()); EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.storage = std::make_shared>(*f.validityBits); EXPECT_FALSE(f.isStructurallyValid()); EXPECT_EQ(validate(contract(), f), particle::FieldProfileValidationCode::InvalidFrame); } TEST(ParticleFieldProfileValidator, EnforcesVelocityDirectionAndValidityCoreRules) { EXPECT_EQ(particle::FieldUnitNone, 0); EXPECT_EQ(particle::FieldUnitMetersPerSecond, 1); EXPECT_EQ(particle::FieldUnitDegrees, 4); auto c = contract(); auto f = frame(); const auto bytes = f.planes.front().storage; f.planes.push_back(plane(100, particle::FieldValueClass::VelocityComponent, particle::FieldUnitMetersPerSecond, 3, particle::FieldComponent::UEast, particle::FieldDType::Int16, false, bytes)); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidVelocityGroup); f.planes.push_back(plane(101, particle::FieldValueClass::VelocityComponent, particle::FieldUnitMetersPerSecond, 3, particle::FieldComponent::VNorth, particle::FieldDType::Int16, false, bytes)); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::Accepted); f.planes.back().unitID = 99; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidCoreUnit); f = frame(); f.planes.push_back(plane(100, particle::FieldValueClass::VelocityComponent, particle::FieldUnitMetersPerSecond, 0, particle::FieldComponent::UEast, particle::FieldDType::Int16, false, bytes)); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidVelocityGroup); f = frame(); f.planes.push_back(plane(100, particle::FieldValueClass::VelocityComponent, particle::FieldUnitMetersPerSecond, 3, particle::FieldComponent::UEast, particle::FieldDType::Int16, false, bytes)); f.planes.push_back(plane(101, particle::FieldValueClass::VelocityComponent, particle::FieldUnitMetersPerSecond, 3, particle::FieldComponent::UEast, particle::FieldDType::Int16, false, bytes)); f.planes.push_back(plane(102, particle::FieldValueClass::VelocityComponent, particle::FieldUnitMetersPerSecond, 3, particle::FieldComponent::VNorth, particle::FieldDType::Int16, false, bytes)); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidVelocityGroup); f = frame(); f.planes.front().key.semanticID = 10; f.planes.front().valueClass = particle::FieldValueClass::DirectionAngle; f.planes.front().unitID = particle::FieldUnitDegrees; f.planes.front().dtype = particle::FieldDType::UInt16; f.planes.front().scale = 360.0f / 65536.0f; c.requiredPlanes.front().semanticID = 10; c.requiredPlanes.front().valueClass = particle::FieldValueClass::DirectionAngle; c.requiredPlanes.front().unitID = particle::FieldUnitDegrees; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::Accepted); f.planes.front().unitID = 2; c.requiredPlanes.front().unitID = 2; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::SemanticContractMismatch); c = contract(); f = frame(); f.validityPlane.dtype = particle::FieldDType::UInt8; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.unitID = 1; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidFrame); f = frame(); f.validityPlane.valueClass = particle::FieldValueClass::Scalar; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidFrame); } TEST(ParticleFieldProfileValidator, EnforcesReservedCoreSemanticContractsFromProtocolTable) { auto c = contract(); auto f = frame(); // Contract and frame agree on the spoof, but reserved semantic 1 remains // VELOCITY_COMPONENT/MPS/U_EAST and cannot become an opaque scalar. c.requiredPlanes.front().semanticID = 1; f.planes.front().key.semanticID = 1; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::SemanticContractMismatch); c = contract(); f = frame(); c.requiredPlanes.front() = {2, particle::FieldValueClass::VelocityComponent, particle::FieldUnitMetersPerSecond, 4, particle::FieldComponent::UEast, true}; f.planes.front() = plane(2, particle::FieldValueClass::VelocityComponent, particle::FieldUnitMetersPerSecond, 4, particle::FieldComponent::UEast, particle::FieldDType::Int16, true, f.planes.front().storage, "u-spoof"); EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::SemanticContractMismatch); c = contract(); f = frame(); c.requiredPlanes.back().semanticID = 3; c.requiredPlanes.back().valueClass = particle::FieldValueClass::Scalar; f.validityPlane.key.semanticID = 3; f.validityPlane.valueClass = particle::FieldValueClass::Scalar; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidFrame); } TEST(ParticleFieldProfileValidator, EnforcesDirectionCircularQuantizationContract) { auto c = contract(); auto f = frame(); c.requiredPlanes.front() = {10, particle::FieldValueClass::DirectionAngle, particle::FieldUnitDegrees, 0, particle::FieldComponent::None, true}; f.planes.front() = plane(10, particle::FieldValueClass::DirectionAngle, particle::FieldUnitDegrees, 0, particle::FieldComponent::None, particle::FieldDType::UInt16, true, f.planes.front().storage, "direction"); f.planes.front().scale = 360.0f / 65536.0f; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::Accepted); f.planes.front().unitID = particle::FieldUnitMetersPerSecond; c.requiredPlanes.front().unitID = particle::FieldUnitMetersPerSecond; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::SemanticContractMismatch); f.planes.front().unitID = particle::FieldUnitDegrees; c.requiredPlanes.front().unitID = particle::FieldUnitDegrees; f.planes.front().key.component = particle::FieldComponent::UEast; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::SemanticContractMismatch); f.planes.front().key.component = particle::FieldComponent::None; f.planes.front().key.groupID = 1; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::SemanticContractMismatch); f.planes.front().key.groupID = 0; f.planes.front().dtype = particle::FieldDType::UInt8; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidCoreUnit); f.planes.front().dtype = particle::FieldDType::UInt16; f.planes.front().scale = 0.1f; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidCoreUnit); f.planes.front().scale = 360.0f / 65536.0f; f.planes.front().offset = 1.0f; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::InvalidCoreUnit); } TEST(ParticleFieldProfileValidator, UsesSemanticAndGroupAsTheOnlyPlaneIdentity) { auto f = frame(); auto duplicate = f.planes.front(); duplicate.key.component = particle::FieldComponent::UEast; f.planes.push_back(duplicate); EXPECT_FALSE(f.isStructurallyValid()); auto c = contract(); auto duplicateRequirement = c.requiredPlanes.front(); duplicateRequirement.component = particle::FieldComponent::UEast; c.requiredPlanes.push_back(duplicateRequirement); EXPECT_FALSE(c.isStructurallyValid()); c = contract(); f = frame(); f.planes.front().key.component = particle::FieldComponent::UEast; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::PlaneDescriptorMismatch); } TEST(ParticleFieldProfileValidator, AppliesSchemaSharedValidityAndTimeRules) { auto c = contract(); auto f = frame(); c.contractSchemaVersion = 2; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::UnsupportedContractSchema); c = contract(); c.sharedValidityRequired = false; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::UnsupportedSharedValidityMode); c = contract(); f.frameContentIdentity.validTimeMs = 999; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::Accepted); c.timeRelation = particle::FieldTimeRelation::ValidGteReference; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::TimeRelationMismatch); f.frameContentIdentity.validTimeMs = 1'000; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::Accepted); c.timeRelation = particle::FieldTimeRelation::ValidEqReference; f.frameContentIdentity.validTimeMs = 1'001; EXPECT_EQ(validate(c, f), particle::FieldProfileValidationCode::TimeRelationMismatch); } TEST(ParticleFieldRuntime, RevisionIdentityAndIndependentCadence) { particle::FieldSeries series; const uint64_t base = 1'700'000'000'000ULL, hour = 3'600'000ULL; EXPECT_EQ(series.submit(runtimeFrame(base, 1, 1, "a")), particle::FieldSeriesSubmitResult::Inserted); EXPECT_EQ(series.submit(runtimeFrame(base, 1, 1, "a")), particle::FieldSeriesSubmitResult::Duplicate); EXPECT_EQ(series.submit(runtimeFrame(base, 1, 1, "changed")), particle::FieldSeriesSubmitResult::RejectedIdentityConflict); EXPECT_EQ(series.submit(runtimeFrame(base, 2, 2, "b")), particle::FieldSeriesSubmitResult::ReplacedRevision); particle::EncodedFieldStore store; EXPECT_EQ(store.submit("wind-key", runtimeFrame(base, 1, 1, "w0", "wind")), particle::FieldSeriesSubmitResult::Inserted); EXPECT_EQ(store.submit("wind-key", runtimeFrame(base + hour, 2, 1, "w1", "wind")), particle::FieldSeriesSubmitResult::Inserted); EXPECT_EQ(store.submit("wave-key", runtimeFrame(base, 1, 1, "a0", "wave")), particle::FieldSeriesSubmitResult::Inserted); EXPECT_EQ(store.submit("wave-key", runtimeFrame(base + 3 * hour, 2, 1, "a3", "wave")), particle::FieldSeriesSubmitResult::Inserted); const auto wind = store.resolve("wind-key", base + hour / 2), wave = store.resolve("wave-key", base + hour / 2); ASSERT_TRUE(wind); ASSERT_TRUE(wave); EXPECT_FLOAT_EQ(wind->mix, 0.5f); EXPECT_FLOAT_EQ(wave->mix, 1.0f / 6.0f); } TEST(ParticleFieldRuntime, ForecastClockOwnsRequestedTimeAndPlayback) { particle::ForecastClock clock; EXPECT_TRUE(clock.setRequestedForecastTime(1'700'000'000'000ULL)); EXPECT_FALSE(clock.advance(1'000)); EXPECT_TRUE(clock.setPlaybackSpeed(2.0f)); clock.play(); EXPECT_TRUE(clock.advance(500)); EXPECT_EQ(clock.requestedForecastTime(), 1'700'000'001'000ULL); clock.pause(); EXPECT_FALSE(clock.advance(500)); EXPECT_FALSE(clock.setPlaybackSpeed(3.0f)); } TEST(ParticleFieldRuntime, ForecastClockPreservesHalfMillisecondRemainder) { particle::ForecastClock clock; ASSERT_TRUE(clock.setRequestedForecastTime(1'000)); ASSERT_TRUE(clock.setPlaybackSpeed(0.5f)); clock.play(); EXPECT_TRUE(clock.advance(1)); EXPECT_EQ(clock.requestedForecastTime(), 1'000); ASSERT_TRUE(clock.setRequestedForecastTime(2'000)); EXPECT_TRUE(clock.advance(1)); EXPECT_EQ(clock.requestedForecastTime(), 2'000); EXPECT_TRUE(clock.advance(1)); EXPECT_EQ(clock.requestedForecastTime(), 2'001); ASSERT_TRUE(clock.setRequestedForecastTime(1'000)); EXPECT_TRUE(clock.advance(1)); EXPECT_EQ(clock.requestedForecastTime(), 1'000); for (size_t i = 0; i < 8; ++i) EXPECT_TRUE(clock.advance(1)); EXPECT_EQ(clock.requestedForecastTime(), 1'004); clock.pause(); EXPECT_FALSE(clock.advance(10)); EXPECT_EQ(clock.requestedForecastTime(), 1'004); ASSERT_TRUE(clock.setRequestedForecastTime(3'000)); ASSERT_TRUE(clock.setPlaybackSpeed(1.0f)); clock.play(); EXPECT_TRUE(clock.advance(3)); EXPECT_EQ(clock.requestedForecastTime(), 3'003); ASSERT_TRUE(clock.setPlaybackSpeed(2.0f)); EXPECT_TRUE(clock.advance(3)); EXPECT_EQ(clock.requestedForecastTime(), 3'009); ASSERT_TRUE(clock.setRequestedForecastTime(std::numeric_limits::max())); EXPECT_FALSE(clock.advance(1)); EXPECT_EQ(clock.requestedForecastTime(), std::numeric_limits::max()); }