blob: a600c44bc1f0c0c7d8a1c1ab70e48304a711c0dd [file]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include <gtest/gtest.h>
#include <pulsar/st/Schema.h>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <span>
#include <string>
#include <vector>
using namespace pulsar::st;
namespace {
// Encode + decode through the public Schema<T> seam and require the value to
// survive the roundtrip.
template <typename T>
void expectRoundtrip(const T& value) {
Schema<T> schema;
std::vector<std::byte> encoded;
auto ok = schema.encode(value, encoded);
ASSERT_TRUE(ok);
auto decoded = schema.decode(std::span<const std::byte>(encoded));
ASSERT_TRUE(decoded);
ASSERT_EQ(*decoded, value);
}
} // namespace
TEST(SchemaCodecTest, testIntegerRoundtrips) {
expectRoundtrip<std::int8_t>(42);
expectRoundtrip<std::int8_t>(-1);
expectRoundtrip<std::int16_t>(0x1234);
expectRoundtrip<std::int16_t>(std::numeric_limits<std::int16_t>::min());
expectRoundtrip<std::int32_t>(0x01020304);
expectRoundtrip<std::int32_t>(-1);
expectRoundtrip<std::int32_t>(std::numeric_limits<std::int32_t>::max());
expectRoundtrip<std::int64_t>(0x0102030405060708LL);
expectRoundtrip<std::int64_t>(std::numeric_limits<std::int64_t>::min());
}
TEST(SchemaCodecTest, testFloatingPointRoundtrips) {
expectRoundtrip<float>(3.5f);
expectRoundtrip<float>(-0.0f);
expectRoundtrip<double>(2.718281828459045);
expectRoundtrip<double>(std::numeric_limits<double>::max());
}
TEST(SchemaCodecTest, testStringRoundtrip) {
expectRoundtrip<std::string>("hello scalable topics");
expectRoundtrip<std::string>("");
}
TEST(SchemaCodecTest, testBytesRoundtrip) {
Bytes payload = {std::byte{0x00}, std::byte{0xFF}, std::byte{0x7F}};
expectRoundtrip<Bytes>(payload);
}
TEST(SchemaCodecTest, testInt32IsBigEndianOnTheWire) {
Schema<std::int32_t> schema;
std::vector<std::byte> encoded;
ASSERT_TRUE(schema.encode(0x01020304, encoded));
ASSERT_EQ(encoded.size(), 4u);
ASSERT_EQ(encoded[0], std::byte{0x01});
ASSERT_EQ(encoded[1], std::byte{0x02});
ASSERT_EQ(encoded[2], std::byte{0x03});
ASSERT_EQ(encoded[3], std::byte{0x04});
}
TEST(SchemaCodecTest, testShortPayloadIsAnErrorNotUb) {
std::vector<std::byte> tooShort(2);
ASSERT_FALSE(Schema<std::int32_t>{}.decode(std::span<const std::byte>(tooShort)));
ASSERT_FALSE(Schema<std::int64_t>{}.decode(std::span<const std::byte>(tooShort)));
ASSERT_FALSE(Schema<float>{}.decode(std::span<const std::byte>(tooShort)));
ASSERT_FALSE(Schema<double>{}.decode(std::span<const std::byte>(tooShort)));
std::vector<std::byte> empty;
ASSERT_FALSE(Schema<std::int8_t>{}.decode(std::span<const std::byte>(empty)));
}
TEST(SchemaCodecTest, testBytesViewDecodeIsZeroCopy) {
std::vector<std::byte> buffer = {std::byte{1}, std::byte{2}, std::byte{3}};
Schema<BytesView> schema;
auto view = schema.decode(std::span<const std::byte>(buffer));
ASSERT_TRUE(view);
ASSERT_EQ(view->data(), buffer.data()); // a view into the same buffer, no copy
ASSERT_EQ(view->size(), buffer.size());
}
TEST(SchemaCodecTest, testPrimitiveSchemaInfoNames) {
ASSERT_EQ(Schema<std::string>{}.info().getName(), "STRING");
ASSERT_EQ(Schema<std::int32_t>{}.info().getName(), "INT32");
ASSERT_EQ(Schema<std::int64_t>{}.info().getName(), "INT64");
ASSERT_EQ(Schema<float>{}.info().getName(), "FLOAT");
ASSERT_EQ(Schema<double>{}.info().getName(), "DOUBLE");
ASSERT_EQ(Schema<Bytes>{}.info().getName(), "BYTES");
}
TEST(SchemaCodecTest, testUnsetSchemaReportsErrors) {
struct NotAPrimitive {
int x = 0;
};
Schema<NotAPrimitive> schema; // no SerDe supplied -> unset codec
std::vector<std::byte> out;
ASSERT_FALSE(schema.encode(NotAPrimitive{}, out));
ASSERT_FALSE(schema.decode(std::span<const std::byte>(out)));
}
TEST(SchemaCodecTest, testCustomSerDeThroughTypeErasure) {
// A toy SerDe proving the pluggable seam: encodes an int as a decimal string.
struct DecimalSerDe {
SchemaInfo info() const { return SchemaInfo(SchemaType::STRING, "DECIMAL", ""); }
Expected<void> encode(const int& v, std::vector<std::byte>& out) const {
const std::string s = std::to_string(v);
const auto* p = reinterpret_cast<const std::byte*>(s.data());
out.assign(p, p + s.size());
return {};
}
Expected<int> decode(std::span<const std::byte> d) const {
return std::stoi(std::string(reinterpret_cast<const char*>(d.data()), d.size()));
}
};
Schema<int> schema{DecimalSerDe{}};
std::vector<std::byte> encoded;
ASSERT_TRUE(schema.encode(12345, encoded));
ASSERT_EQ(encoded.size(), 5u); // "12345"
auto decoded = schema.decode(std::span<const std::byte>(encoded));
ASSERT_TRUE(decoded);
ASSERT_EQ(*decoded, 12345);
}