feat: add QBIN V3 decoder for particle.9
This commit is contained in:
@@ -22,6 +22,7 @@ add_library(
|
||||
${PROJECT_SOURCE_DIR}/test/particle/field_binding.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/particle/field_profile_contract_json.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/particle/qbin_v2_decoder.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/particle/qbin_v3_decoder.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/particle/encoded_field_transport.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/plugin/plugin.test.cpp
|
||||
${PROJECT_SOURCE_DIR}/test/renderer/image_manager.test.cpp
|
||||
|
||||
618
test/particle/qbin_v3_decoder.test.cpp
Normal file
618
test/particle/qbin_v3_decoder.test.cpp
Normal file
@@ -0,0 +1,618 @@
|
||||
#include <mbgl/particle/ingress/qbin_v3_decoder.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
using namespace mbgl;
|
||||
namespace {
|
||||
|
||||
constexpr size_t Header = 112;
|
||||
constexpr size_t Descriptor = 32;
|
||||
|
||||
void u16(std::vector<uint8_t>& b, size_t o, uint16_t v) {
|
||||
b[o] = uint8_t(v);
|
||||
b[o + 1] = uint8_t(v >> 8);
|
||||
}
|
||||
void u32(std::vector<uint8_t>& b, size_t o, uint32_t v) {
|
||||
for (size_t i = 0; i < 4; ++i) b[o + i] = uint8_t(v >> (8 * i));
|
||||
}
|
||||
uint32_t r32(const std::vector<uint8_t>& b, size_t o) {
|
||||
uint32_t v = 0;
|
||||
for (size_t i = 0; i < 4; ++i) v |= uint32_t(b[o + i]) << (8 * i);
|
||||
return v;
|
||||
}
|
||||
void u64(std::vector<uint8_t>& b, size_t o, uint64_t v) {
|
||||
for (size_t i = 0; i < 8; ++i) b[o + i] = uint8_t(v >> (8 * i));
|
||||
}
|
||||
void i64(std::vector<uint8_t>& b, size_t o, int64_t v) {
|
||||
u64(b, o, static_cast<uint64_t>(v));
|
||||
}
|
||||
void f32(std::vector<uint8_t>& b, size_t o, float v) {
|
||||
uint32_t bits = 0;
|
||||
std::memcpy(&bits, &v, sizeof(bits));
|
||||
u32(b, o, bits);
|
||||
}
|
||||
void f64(std::vector<uint8_t>& b, size_t o, double v) {
|
||||
uint64_t bits = 0;
|
||||
std::memcpy(&bits, &v, sizeof(bits));
|
||||
u64(b, o, bits);
|
||||
}
|
||||
size_t a16(size_t x) { return (x + 15u) & ~size_t(15u); }
|
||||
uint32_t crc(const std::vector<uint8_t>& b) {
|
||||
uint32_t c = ~0u;
|
||||
for (size_t i = 0; i < b.size(); ++i) {
|
||||
const uint8_t x = i >= 76 && i < 80 ? 0 : b[i];
|
||||
c ^= x;
|
||||
for (int j = 0; j < 8; ++j) c = (c >> 1) ^ ((c & 1u) ? 0xedb88320u : 0);
|
||||
}
|
||||
return ~c;
|
||||
}
|
||||
void recalc(std::vector<uint8_t>& b) { u32(b, 76, crc(b)); }
|
||||
bool validAt(const std::vector<uint8_t>& v, uint64_t i) {
|
||||
return ((v[i >> 3] >> (i & 7u)) & 1u) != 0;
|
||||
}
|
||||
int64_t normDir(int64_t x, uint32_t n) {
|
||||
if (x < 0) x += n;
|
||||
else if (x >= int64_t(n)) x -= n;
|
||||
return x;
|
||||
}
|
||||
uint64_t zigzag(int64_t r) {
|
||||
return r >= 0 ? uint64_t(2 * r) : uint64_t(-2 * r - 1);
|
||||
}
|
||||
|
||||
struct Bits {
|
||||
std::vector<uint8_t> bytes;
|
||||
size_t pos = 0;
|
||||
void bit(uint8_t value) {
|
||||
if ((pos & 7u) == 0) bytes.push_back(0);
|
||||
if (value) bytes.back() |= uint8_t(1u << (pos & 7u));
|
||||
++pos;
|
||||
}
|
||||
void bits(uint32_t value, uint8_t count) {
|
||||
for (uint8_t i = 0; i < count; ++i) bit((value >> i) & 1u);
|
||||
}
|
||||
};
|
||||
|
||||
struct PlaneSpec {
|
||||
uint16_t semantic = 1;
|
||||
uint8_t dtype = 1;
|
||||
uint8_t flags = 1;
|
||||
float scale = 1.0f;
|
||||
float offset = 0.0f;
|
||||
uint16_t unit = 0;
|
||||
uint16_t group = 0;
|
||||
uint8_t valueClass = 0;
|
||||
uint8_t component = 0;
|
||||
std::vector<int64_t> values;
|
||||
};
|
||||
|
||||
int64_t predict(const PlaneSpec& p,
|
||||
const std::vector<uint8_t>& validity,
|
||||
uint32_t width,
|
||||
uint32_t row,
|
||||
uint32_t col,
|
||||
uint32_t n) {
|
||||
const uint64_t i = uint64_t(row) * width + col;
|
||||
const bool left = col > 0 && validAt(validity, i - 1);
|
||||
const bool up = row > 0 && validAt(validity, i - width);
|
||||
const bool upperLeft = row > 0 && col > 0 && validAt(validity, i - width - 1);
|
||||
int64_t predicted = 0;
|
||||
if (left && up && upperLeft) {
|
||||
predicted = p.values[i - 1] + p.values[i - width] - p.values[i - width - 1];
|
||||
} else if (left) {
|
||||
predicted = p.values[i - 1];
|
||||
} else if (up) {
|
||||
predicted = p.values[i - width];
|
||||
}
|
||||
return n ? normDir(predicted, n) : predicted;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> encodePlane(const PlaneSpec& p,
|
||||
const std::vector<uint8_t>& validity,
|
||||
uint32_t width,
|
||||
uint32_t height,
|
||||
std::optional<uint8_t> forcedK = std::nullopt) {
|
||||
const uint32_t n = p.valueClass == 2
|
||||
? uint32_t(std::floor(360.0 / double(p.scale) + 0.5))
|
||||
: 0;
|
||||
std::vector<uint8_t> out;
|
||||
for (uint32_t rb = 0; rb < height; rb += 16) {
|
||||
for (uint32_t cb = 0; cb < width; cb += 16) {
|
||||
std::vector<uint64_t> symbols;
|
||||
for (uint32_t row = rb; row < std::min(rb + 16u, height); ++row) {
|
||||
for (uint32_t col = cb; col < std::min(cb + 16u, width); ++col) {
|
||||
const uint64_t i = uint64_t(row) * width + col;
|
||||
if (!validAt(validity, i)) continue;
|
||||
const int64_t predicted = predict(p, validity, width, row, col, n);
|
||||
int64_t residual = p.values[i] - predicted;
|
||||
if (n) {
|
||||
int64_t m = residual;
|
||||
if (m < 0) m += n;
|
||||
residual = 2 * m > int64_t(n) ? m - n
|
||||
: 2 * m < int64_t(n) ? m
|
||||
: -int64_t(n / 2);
|
||||
}
|
||||
symbols.push_back(zigzag(residual));
|
||||
}
|
||||
}
|
||||
if (symbols.empty()) continue;
|
||||
|
||||
uint8_t bestK = forcedK.value_or(0);
|
||||
if (!forcedK) {
|
||||
uint64_t bestBits = ~uint64_t(0);
|
||||
for (uint8_t k = 0; k <= 31; ++k) {
|
||||
uint64_t bits = 0;
|
||||
for (const auto z : symbols) bits += (z >> k) + 1u + k;
|
||||
if (bits < bestBits) {
|
||||
bestBits = bits;
|
||||
bestK = k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.push_back(bestK);
|
||||
Bits writer;
|
||||
for (const auto z : symbols) {
|
||||
const uint64_t q = z >> bestK;
|
||||
for (uint64_t j = 0; j < q; ++j) writer.bit(0);
|
||||
writer.bit(1);
|
||||
if (bestK) writer.bits(uint32_t(z & ((uint64_t(1) << bestK) - 1u)), bestK);
|
||||
}
|
||||
out.insert(out.end(), writer.bytes.begin(), writer.bytes.end());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> rawBytes(const PlaneSpec& p, const std::vector<uint8_t>& validity) {
|
||||
const size_t stride = p.dtype == 1 || p.dtype == 2 ? 2u : 1u;
|
||||
std::vector<uint8_t> out(p.values.size() * stride, 0);
|
||||
for (size_t i = 0; i < p.values.size(); ++i) {
|
||||
if (!validAt(validity, i)) continue;
|
||||
if (stride == 2) {
|
||||
const uint16_t v = static_cast<uint16_t>(p.values[i]);
|
||||
out[i * 2] = uint8_t(v);
|
||||
out[i * 2 + 1] = uint8_t(v >> 8);
|
||||
} else {
|
||||
out[i] = static_cast<uint8_t>(p.values[i]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> fixture(uint32_t width,
|
||||
uint32_t height,
|
||||
std::vector<uint8_t> validity,
|
||||
std::vector<PlaneSpec> planes,
|
||||
std::optional<uint8_t> forcedK = std::nullopt,
|
||||
uint16_t fieldType = 999) {
|
||||
const uint64_t cells = uint64_t(width) * height;
|
||||
const uint16_t count = uint16_t(planes.size() + 1);
|
||||
const size_t payloadOffset = Header + size_t(count) * Descriptor;
|
||||
|
||||
struct Encoded { PlaneSpec p; std::vector<uint8_t> bytes; };
|
||||
std::vector<Encoded> encoded;
|
||||
for (auto& p : planes) encoded.push_back({p, encodePlane(p, validity, width, height, forcedK)});
|
||||
|
||||
size_t payloadLength = 0;
|
||||
for (const auto& e : encoded) payloadLength = a16(payloadLength + e.bytes.size());
|
||||
payloadLength = a16(payloadLength + validity.size());
|
||||
std::vector<uint8_t> b(payloadOffset + payloadLength, 0);
|
||||
std::memcpy(b.data(), "QBV3", 4);
|
||||
b[4] = 3;
|
||||
b[5] = 1;
|
||||
u16(b, 6, fieldType);
|
||||
b[8] = b[9] = b[10] = 1;
|
||||
u16(b, 12, count);
|
||||
u32(b, 16, width);
|
||||
u32(b, 20, height);
|
||||
i64(b, 24, 100);
|
||||
i64(b, 32, 90);
|
||||
f64(b, 40, -10);
|
||||
f64(b, 48, -10);
|
||||
f64(b, 56, 10);
|
||||
f64(b, 64, 10);
|
||||
u32(b, 72, payloadLength);
|
||||
u16(b, 80, 0xffff);
|
||||
|
||||
size_t off = 0;
|
||||
for (size_t i = 0; i < encoded.size(); ++i) {
|
||||
const auto& e = encoded[i];
|
||||
const size_t d = Header + i * Descriptor;
|
||||
u16(b, d, e.p.semantic);
|
||||
b[d + 2] = e.p.dtype;
|
||||
b[d + 3] = e.p.flags;
|
||||
f32(b, d + 4, e.p.scale);
|
||||
f32(b, d + 8, e.p.offset);
|
||||
u32(b, d + 12, cells);
|
||||
u32(b, d + 16, off);
|
||||
u32(b, d + 20, e.bytes.size());
|
||||
u16(b, d + 24, e.p.unit);
|
||||
u16(b, d + 26, e.p.group);
|
||||
b[d + 28] = e.p.valueClass;
|
||||
b[d + 29] = e.p.component;
|
||||
std::copy(e.bytes.begin(), e.bytes.end(), b.begin() + payloadOffset + off);
|
||||
off = a16(off + e.bytes.size());
|
||||
}
|
||||
|
||||
const size_t d = Header + encoded.size() * Descriptor;
|
||||
u16(b, d, 3);
|
||||
b[d + 2] = 6;
|
||||
b[d + 3] = 1;
|
||||
f32(b, d + 4, 1.0f);
|
||||
f32(b, d + 8, 0.0f);
|
||||
u32(b, d + 12, cells);
|
||||
u32(b, d + 16, off);
|
||||
u32(b, d + 20, validity.size());
|
||||
b[d + 28] = 3;
|
||||
std::copy(validity.begin(), validity.end(), b.begin() + payloadOffset + off);
|
||||
recalc(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> fixtureV2(uint32_t width,
|
||||
uint32_t height,
|
||||
const std::vector<uint8_t>& validity,
|
||||
const std::vector<PlaneSpec>& planes,
|
||||
uint16_t fieldType = 999) {
|
||||
const uint64_t cells = uint64_t(width) * height;
|
||||
const uint16_t count = uint16_t(planes.size() + 1);
|
||||
const size_t payloadOffset = Header + size_t(count) * Descriptor;
|
||||
|
||||
struct RawPlane {
|
||||
PlaneSpec p;
|
||||
std::vector<uint8_t> bytes;
|
||||
};
|
||||
std::vector<RawPlane> raws;
|
||||
for (const auto& p : planes) raws.push_back({p, rawBytes(p, validity)});
|
||||
|
||||
size_t payloadLength = 0;
|
||||
for (const auto& r : raws) payloadLength = a16(payloadLength + r.bytes.size());
|
||||
payloadLength = a16(payloadLength + validity.size());
|
||||
|
||||
std::vector<uint8_t> b(payloadOffset + payloadLength, 0);
|
||||
std::memcpy(b.data(), "QBV2", 4);
|
||||
b[4] = 2;
|
||||
b[5] = 1;
|
||||
u16(b, 6, fieldType);
|
||||
b[8] = b[9] = b[10] = 1;
|
||||
u16(b, 12, count);
|
||||
u32(b, 16, width);
|
||||
u32(b, 20, height);
|
||||
i64(b, 24, 100);
|
||||
i64(b, 32, 90);
|
||||
f64(b, 40, -10);
|
||||
f64(b, 48, -10);
|
||||
f64(b, 56, 10);
|
||||
f64(b, 64, 10);
|
||||
u32(b, 72, payloadLength);
|
||||
u16(b, 80, 0xffff);
|
||||
|
||||
size_t off = 0;
|
||||
for (size_t i = 0; i < raws.size(); ++i) {
|
||||
const auto& r = raws[i];
|
||||
const size_t d = Header + i * Descriptor;
|
||||
u16(b, d, r.p.semantic);
|
||||
b[d + 2] = r.p.dtype;
|
||||
b[d + 3] = r.p.flags;
|
||||
f32(b, d + 4, r.p.scale);
|
||||
f32(b, d + 8, r.p.offset);
|
||||
u32(b, d + 12, cells);
|
||||
u32(b, d + 16, off);
|
||||
u32(b, d + 20, r.bytes.size());
|
||||
u16(b, d + 24, r.p.unit);
|
||||
u16(b, d + 26, r.p.group);
|
||||
b[d + 28] = r.p.valueClass;
|
||||
b[d + 29] = r.p.component;
|
||||
std::copy(r.bytes.begin(), r.bytes.end(), b.begin() + payloadOffset + off);
|
||||
off = a16(off + r.bytes.size());
|
||||
}
|
||||
|
||||
const size_t d = Header + raws.size() * Descriptor;
|
||||
u16(b, d, 3);
|
||||
b[d + 2] = 6;
|
||||
b[d + 3] = 1;
|
||||
f32(b, d + 4, 1.0f);
|
||||
f32(b, d + 8, 0.0f);
|
||||
u32(b, d + 12, cells);
|
||||
u32(b, d + 16, off);
|
||||
u32(b, d + 20, validity.size());
|
||||
b[d + 28] = 3;
|
||||
std::copy(validity.begin(), validity.end(), b.begin() + payloadOffset + off);
|
||||
recalc(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> validityBits(uint32_t cells, bool allValid = true) {
|
||||
std::vector<uint8_t> v((cells + 7u) / 8u, allValid ? 0xffu : 0u);
|
||||
if (allValid && (cells & 7u)) v.back() = uint8_t((1u << (cells & 7u)) - 1u);
|
||||
return v;
|
||||
}
|
||||
|
||||
particle::ingress::EncodedFieldRuntimeMetadata meta() {
|
||||
return {"qbv3-test", 1, 1};
|
||||
}
|
||||
|
||||
using Code = particle::ingress::QbinDecodeErrorCode;
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ParticleQbinV3Decoder, DecodesAcrossBlockBoundaryAndDirectionWrap) {
|
||||
constexpr uint32_t width = 17, height = 17, cells = width * height;
|
||||
auto validity = validityBits(cells);
|
||||
validity[3] &= uint8_t(~(1u << 1)); // one invalid cell
|
||||
std::vector<int64_t> scalar(cells), direction(cells);
|
||||
for (uint32_t r = 0; r < height; ++r) for (uint32_t c = 0; c < width; ++c) {
|
||||
const size_t i = r * width + c;
|
||||
scalar[i] = int64_t(r) * 40 - int64_t(c) * 7;
|
||||
direction[i] = (350 + int64_t(c) * 3 + int64_t(r) * 11) % 360;
|
||||
}
|
||||
PlaneSpec a{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, scalar};
|
||||
PlaneSpec d{2, 2, 1, 1.0f, 0.0f, 4, 0, 2, 0, direction};
|
||||
auto bytes = fixture(width, height, validity, {a, d});
|
||||
auto result = particle::ingress::QbinV3Decoder::decode(meta(), bytes);
|
||||
ASSERT_TRUE(result.accepted()) << result.detail;
|
||||
ASSERT_EQ(result.frame->planes.size(), 2u);
|
||||
EXPECT_EQ(*result.frame->planes[0].storage, rawBytes(a, validity));
|
||||
EXPECT_EQ(*result.frame->planes[1].storage, rawBytes(d, validity));
|
||||
EXPECT_EQ(*result.frame->validityBits, validity);
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, AcceptsAllInvalidWithSharedZeroLengthOffsets) {
|
||||
constexpr uint32_t width = 2, height = 2, cells = 4;
|
||||
auto validity = validityBits(cells, false);
|
||||
PlaneSpec a{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {1,2,3,4}};
|
||||
PlaneSpec d{2, 2, 1, 1.0f, 0.0f, 4, 0, 2, 0, {0,90,180,270}};
|
||||
auto bytes = fixture(width, height, validity, {a, d});
|
||||
const size_t d0 = Header, d1 = Header + Descriptor, dv = Header + 2 * Descriptor;
|
||||
EXPECT_EQ(r32(bytes, d0 + 16), 0u);
|
||||
EXPECT_EQ(r32(bytes, d1 + 16), 0u);
|
||||
EXPECT_EQ(r32(bytes, dv + 16), 0u);
|
||||
auto result = particle::ingress::QbinV3Decoder::decode(meta(), bytes);
|
||||
ASSERT_TRUE(result.accepted()) << result.detail;
|
||||
EXPECT_EQ(*result.frame->planes[0].storage, std::vector<uint8_t>(cells * 2, 0));
|
||||
EXPECT_EQ(*result.frame->planes[1].storage, std::vector<uint8_t>(cells * 2, 0));
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, AcceptsNonOptimalRiceK) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {0,0,0,0}};
|
||||
auto bytes = fixture(2, 2, validity, {p}, uint8_t(5));
|
||||
auto result = particle::ingress::QbinV3Decoder::decode(meta(), bytes);
|
||||
EXPECT_TRUE(result.accepted()) << result.detail;
|
||||
}
|
||||
|
||||
|
||||
TEST(ParticleQbinV3Decoder, RejectsCrcDescriptorAndValidityCorruption) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {1,2,3,4}};
|
||||
|
||||
auto bytes = fixture(2, 2, validity, {p});
|
||||
bytes[6] ^= 1;
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::CrcMismatch);
|
||||
|
||||
bytes = fixture(2, 2, validity, {p});
|
||||
bytes[Header + 2] = 5;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::InvalidDescriptor);
|
||||
|
||||
bytes = fixture(2, 2, validity, {p});
|
||||
const size_t validityDescriptor = Header + Descriptor;
|
||||
bytes[validityDescriptor + 2] = 3;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::InvalidValidity);
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, RejectsMalformedRiceAndNonzeroBlockPadding) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {0,0,0,0}};
|
||||
|
||||
auto bytes = fixture(2, 2, validity, {p});
|
||||
const size_t payload = Header + 2 * Descriptor;
|
||||
bytes[payload] = 32;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::MalformedRice);
|
||||
|
||||
auto oneValid = validityBits(4, false);
|
||||
oneValid[0] = 0x01;
|
||||
bytes = fixture(2, 2, oneValid, {p}, uint8_t(0));
|
||||
bytes[payload + 1] |= 0x80;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::MalformedRice);
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, RejectsResidualOutsideLogicalRange) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec p{1, 3, 1, 1.0f, 0.0f, 77, 0, 0, 0, {0,0,0,0}};
|
||||
auto bytes = fixture(2, 2, validity, {p}, uint8_t(31));
|
||||
const size_t payload = Header + 2 * Descriptor;
|
||||
ASSERT_GT(bytes.size(), payload + 4);
|
||||
bytes[payload + 4] |= 0x80;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::ResidualOutOfRange);
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, PreservesNodeRegisteredGridGeometry) {
|
||||
auto validity = validityBits(9);
|
||||
PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, std::vector<int64_t>(9, 0)};
|
||||
auto bytes = fixture(3, 3, validity, {p});
|
||||
auto result = particle::ingress::QbinV3Decoder::decode(meta(), bytes);
|
||||
ASSERT_TRUE(result.accepted()) << result.detail;
|
||||
|
||||
const auto& g = result.frame->grid;
|
||||
EXPECT_EQ(g.width, 3u);
|
||||
EXPECT_EQ(g.height, 3u);
|
||||
EXPECT_DOUBLE_EQ(g.west, -10.0);
|
||||
EXPECT_DOUBLE_EQ(g.south, -10.0);
|
||||
EXPECT_DOUBLE_EQ(g.east, 10.0);
|
||||
EXPECT_DOUBLE_EQ(g.north, 10.0);
|
||||
EXPECT_DOUBLE_EQ(g.west + (g.east - g.west) * 1.0 / (g.width - 1), 0.0);
|
||||
EXPECT_DOUBLE_EQ(g.south + (g.north - g.south) * 1.0 / (g.height - 1), 0.0);
|
||||
}
|
||||
|
||||
TEST(ParticleEncodedFieldIngress, DispatchesQbinV3AndRejectsUnknownMagic) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec u{1, 1, 1, 1.0f, 0.0f, 1, 1, 1, 1, {1,2,3,4}};
|
||||
PlaneSpec v{2, 1, 1, 1.0f, 0.0f, 1, 1, 1, 2, {5,6,7,8}};
|
||||
auto bytes = fixture(2, 2, validity, {u, v});
|
||||
|
||||
particle::ResolvedFieldProfileContract contract;
|
||||
contract.profileID = "qbv3.wind.test";
|
||||
contract.profileVersion = 1;
|
||||
contract.requiredPlanes = {
|
||||
{1, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::UEast, true},
|
||||
{2, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::VNorth, true},
|
||||
{3, particle::FieldValueClass::Validity, 0, 0, particle::FieldComponent::None, true},
|
||||
};
|
||||
|
||||
auto accepted = particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(), &contract, bytes);
|
||||
EXPECT_TRUE(accepted.accepted()) << accepted.detail;
|
||||
|
||||
bytes[0] = 'X';
|
||||
recalc(bytes);
|
||||
auto rejected = particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(), &contract, bytes);
|
||||
EXPECT_EQ(rejected.code, particle::ingress::EncodedFieldIngressCode::DecodeRejected);
|
||||
EXPECT_EQ(rejected.decodeCode, Code::BadMagic);
|
||||
}
|
||||
|
||||
|
||||
TEST(ParticleQbinV3Decoder, CoversAllCompressedIntegerDtypes) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec u8{1, 3, 1, 0.5f, 1.0f, 77, 0, 0, 0, {0, 255, 128, 1}};
|
||||
PlaneSpec i8{2, 4, 1, 2.0f, -5.0f, 78, 0, 0, 0, {-128, 127, -1, 0}};
|
||||
auto bytes = fixture(2, 2, validity, {u8, i8});
|
||||
auto result = particle::ingress::QbinV3Decoder::decode(meta(), bytes);
|
||||
ASSERT_TRUE(result.accepted()) << result.detail;
|
||||
ASSERT_EQ(result.frame->planes.size(), 2u);
|
||||
EXPECT_EQ(*result.frame->planes[0].storage, rawBytes(u8, validity));
|
||||
EXPECT_EQ(*result.frame->planes[1].storage, rawBytes(i8, validity));
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, AcceptsOpaqueFieldTypeZeroAndUnknown) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {1, 2, 3, 4}};
|
||||
EXPECT_TRUE(particle::ingress::QbinV3Decoder::decode(meta(), fixture(2, 2, validity, {p}, std::nullopt, 0)).accepted());
|
||||
EXPECT_TRUE(particle::ingress::QbinV3Decoder::decode(meta(), fixture(2, 2, validity, {p}, std::nullopt, 65535)).accepted());
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, RejectsHeaderVersionEndianReservedBoundsAndNegativeZero) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {1, 2, 3, 4}};
|
||||
|
||||
auto bytes = fixture(2, 2, validity, {p});
|
||||
bytes[0] = 'X';
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::BadMagic);
|
||||
|
||||
bytes = fixture(2, 2, validity, {p});
|
||||
bytes[4] = 2;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::UnsupportedVersion);
|
||||
|
||||
bytes = fixture(2, 2, validity, {p});
|
||||
bytes[11] = 1;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::ReservedNonZero);
|
||||
|
||||
bytes = fixture(2, 2, validity, {p});
|
||||
bytes[16] = 0;
|
||||
bytes[17] = 0;
|
||||
bytes[18] = 0;
|
||||
bytes[19] = 2;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::LimitExceeded);
|
||||
|
||||
bytes = fixture(2, 2, validity, {p});
|
||||
f64(bytes, 40, 11.0);
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::InvalidBounds);
|
||||
|
||||
bytes = fixture(2, 2, validity, {p});
|
||||
u64(bytes, 40, 0x8000000000000000ull);
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::InvalidBounds);
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, RejectsDescriptorOrderDuplicateAndPacking) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec u{1, 1, 1, 1.0f, 0.0f, 1, 1, 1, 1, {1, 2, 3, 4}};
|
||||
PlaneSpec v{2, 1, 1, 1.0f, 0.0f, 1, 1, 1, 2, {5, 6, 7, 8}};
|
||||
|
||||
auto bytes = fixture(2, 2, validity, {u, v});
|
||||
u16(bytes, Header, 2);
|
||||
u16(bytes, Header + Descriptor, 1);
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::NonCanonicalDescriptorOrder);
|
||||
|
||||
bytes = fixture(2, 2, validity, {u, v});
|
||||
u16(bytes, Header + Descriptor, 1);
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::DuplicatePlaneKey);
|
||||
|
||||
bytes = fixture(2, 2, validity, {u, v});
|
||||
u32(bytes, Header + Descriptor + 16, 0);
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::NonCanonicalPacking);
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, RejectsUnterminatedUnaryExtraEncodedByteAndRawOverflow) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec zero{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {0, 0, 0, 0}};
|
||||
const size_t payload = Header + 2 * Descriptor;
|
||||
|
||||
auto bytes = fixture(2, 2, validity, {zero}, uint8_t(0));
|
||||
bytes[payload + 1] = 0;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::MalformedRice);
|
||||
|
||||
bytes = fixture(2, 2, validity, {zero}, uint8_t(0));
|
||||
const uint32_t length = r32(bytes, Header + 20);
|
||||
ASSERT_LT(payload + length, bytes.size());
|
||||
u32(bytes, Header + 20, length + 1);
|
||||
bytes[payload + length] = 0;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::MalformedRice);
|
||||
|
||||
PlaneSpec overflow{1, 3, 1, 1.0f, 0.0f, 77, 0, 0, 0, {300, 0, 0, 0}};
|
||||
bytes = fixture(2, 2, validity, {overflow});
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::InvalidCellPayload);
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, EnforcesDirectionHalfCircleNegativeTie) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec direction{1, 2, 1, 90.0f, 0.0f, 4, 0, 2, 0, {0, 2, 0, 2}};
|
||||
auto bytes = fixture(2, 2, validity, {direction}, uint8_t(0));
|
||||
auto canonical = particle::ingress::QbinV3Decoder::decode(meta(), bytes);
|
||||
ASSERT_TRUE(canonical.accepted()) << canonical.detail;
|
||||
|
||||
const size_t payload = Header + 2 * Descriptor;
|
||||
ASSERT_EQ(r32(bytes, Header + 20), 2u);
|
||||
// canonical bits are 0x71: 0, -2, 0, 0. Replace the -2 symbol
|
||||
// with the forbidden +2 half-circle representation (z=4), yielding 0xe1.
|
||||
bytes[payload + 1] = 0xe1;
|
||||
recalc(bytes);
|
||||
EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::ResidualOutOfRange);
|
||||
}
|
||||
|
||||
TEST(ParticleQbinV3Decoder, ContentIdentityMatchesQbinV2CanonicalFrame) {
|
||||
auto validity = validityBits(4);
|
||||
PlaneSpec u{1, 1, 1, 0.25f, 0.0f, 1, 1, 1, 1, {1, -2, 3, -4}};
|
||||
PlaneSpec v{2, 1, 1, 0.25f, 0.0f, 1, 1, 1, 2, {5, 6, -7, 8}};
|
||||
auto v2 = particle::ingress::QbinV2Decoder::decode(meta(), fixtureV2(2, 2, validity, {u, v}));
|
||||
auto v3 = particle::ingress::QbinV3Decoder::decode(meta(), fixture(2, 2, validity, {u, v}));
|
||||
ASSERT_TRUE(v2.accepted()) << v2.detail;
|
||||
ASSERT_TRUE(v3.accepted()) << v3.detail;
|
||||
ASSERT_EQ(v2.frame->planes.size(), v3.frame->planes.size());
|
||||
for (size_t i = 0; i < v2.frame->planes.size(); ++i) {
|
||||
EXPECT_EQ(*v2.frame->planes[i].storage, *v3.frame->planes[i].storage);
|
||||
EXPECT_EQ(v2.frame->planes[i].contentIdentity, v3.frame->planes[i].contentIdentity);
|
||||
}
|
||||
EXPECT_EQ(*v2.frame->validityBits, *v3.frame->validityBits);
|
||||
EXPECT_EQ(v2.frame->validityContentIdentity, v3.frame->validityContentIdentity);
|
||||
}
|
||||
Reference in New Issue
Block a user