From 2690ba6ad1f92320180f562448e888329405d821 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 26 Aug 2026 19:53:18 +0200 Subject: [PATCH] test: add tests for varint and byte-buffer codec utilities --- tests/testCodec.cpp | 231 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 tests/testCodec.cpp diff --git a/tests/testCodec.cpp b/tests/testCodec.cpp new file mode 100644 index 0000000..55a6b84 --- /dev/null +++ b/tests/testCodec.cpp @@ -0,0 +1,231 @@ +#include "testHarness.h" +#include "vf/Codec.h" + +#include +#include +#include + +using namespace vf; + +namespace { + +// Deterministic pseudo-random generator so every run exercises the same data. +struct Lcg { + uint64_t state; + explicit Lcg(uint64_t seed) : state(seed) {} + uint32_t next() { + state = state * 6364136223846793005ull + 1442695040888963407ull; + return static_cast(state >> 32); + } + uint8_t byte(int modulo) { return static_cast(next() % static_cast(modulo)); } +}; + +ByteBuffer roundTripCodec(const ByteBuffer& input) { + ByteBuffer packed = compress(input); + ByteBuffer restored; + bool ok = decompress(packed.data(), packed.size(), restored); + CHECK(ok); + return restored; +} + +} // namespace + +VF_TEST(codecVarintUnsignedRoundTrip) { + const uint64_t samples[] = {0, 1, 127, 128, 255, 300, 16383, 16384, + 1u << 21, 0xFFFFFFFFull, 0x1234567890ABCDEFull, + 0xFFFFFFFFFFFFFFFFull}; + for (uint64_t v : samples) { + ByteWriter w; + w.putVarU64(v); + ByteReader r(w.bytes().data(), w.bytes().size()); + uint64_t got = 0; + CHECK(r.getVarU64(got)); + CHECK_EQ(got, v); + CHECK(r.exhausted()); + } +} + +VF_TEST(codecZigzagRoundTrip) { + const int64_t samples[] = {0, -1, 1, -2, 2, 63, -64, 2147483647LL, + -2147483648LL, 9223372036854775807LL, + -9223372036854775807LL - 1}; + for (int64_t v : samples) { + ByteWriter w; + w.putVarI64(v); + ByteReader r(w.bytes().data(), w.bytes().size()); + int64_t got = 0; + CHECK(r.getVarI64(got)); + CHECK_EQ(got, v); + } + for (int32_t v = -5000; v <= 5000; v += 7) { + CHECK_EQ(zigzagDecode32(zigzagEncode32(v)), v); + } +} + +VF_TEST(codecMixedWriterReader) { + ByteWriter w; + w.putU8(0xAB); + w.putU16(0xBEEF); + w.putU32(0xDEADBEEF); + w.putU64(0x0123456789ABCDEFull); + w.putFloat(3.14159f); + w.putDouble(2.718281828459045); + w.putString("voxelforge"); + w.putVarI32(-123456); + + ByteReader r(w.bytes().data(), w.bytes().size()); + uint8_t a; + uint16_t b; + uint32_t c; + uint64_t d; + float e; + double f; + std::string g; + int32_t h; + CHECK(r.getU8(a) && a == 0xAB); + CHECK(r.getU16(b) && b == 0xBEEF); + CHECK(r.getU32(c) && c == 0xDEADBEEF); + CHECK(r.getU64(d) && d == 0x0123456789ABCDEFull); + CHECK(r.getFloat(e) && e == 3.14159f); + CHECK(r.getDouble(f) && f == 2.718281828459045); + CHECK(r.getString(g) && g == "voxelforge"); + CHECK(r.getVarI32(h) && h == -123456); + CHECK(r.exhausted()); +} + +VF_TEST(codecReaderRejectsTruncated) { + ByteWriter w; + w.putU32(42); + ByteReader r(w.bytes().data(), w.bytes().size()); + uint64_t big; + CHECK(!r.getU64(big)); + CHECK(!r.ok()); +} + +VF_TEST(rleCompressesRuns) { + ByteBuffer input(4096, 0x7); + ByteBuffer encoded = rleEncode(input.data(), input.size()); + CHECK(encoded.size() < input.size() / 8); + ByteBuffer decoded; + CHECK(rleDecode(encoded.data(), encoded.size(), decoded)); + CHECK(decoded == input); +} + +VF_TEST(rleHandlesMixedAndLiterals) { + Lcg rng(0x1234); + ByteBuffer input; + for (int block = 0; block < 200; ++block) { + if (block % 3 == 0) { + uint8_t v = rng.byte(256); + int len = 3 + static_cast(rng.byte(200)); + input.insert(input.end(), static_cast(len), v); + } else { + int len = 1 + static_cast(rng.byte(64)); + for (int i = 0; i < len; ++i) input.push_back(rng.byte(256)); + } + } + ByteBuffer encoded = rleEncode(input.data(), input.size()); + ByteBuffer decoded; + CHECK(rleDecode(encoded.data(), encoded.size(), decoded)); + CHECK(decoded == input); +} + +VF_TEST(rleEmptyRoundTrip) { + ByteBuffer encoded = rleEncode(nullptr, 0); + ByteBuffer decoded; + CHECK(rleDecode(encoded.data(), encoded.size(), decoded)); + CHECK(decoded.empty()); +} + +VF_TEST(lz77CompressesRepetition) { + ByteBuffer input; + const char* phrase = "the quick brown fox jumps over the lazy dog. "; + for (int i = 0; i < 400; ++i) + for (const char* p = phrase; *p; ++p) input.push_back(static_cast(*p)); + + ByteBuffer encoded = lz77Compress(input.data(), input.size()); + CHECK(encoded.size() < input.size() / 4); + ByteBuffer decoded; + CHECK(lz77Decompress(encoded.data(), encoded.size(), decoded)); + CHECK(decoded == input); +} + +VF_TEST(lz77HandlesOverlappingRuns) { + ByteBuffer input(1000, 0x42); + input[0] = 0x1; + ByteBuffer encoded = lz77Compress(input.data(), input.size()); + ByteBuffer decoded; + CHECK(lz77Decompress(encoded.data(), encoded.size(), decoded)); + CHECK(decoded == input); +} + +VF_TEST(lz77RandomDataRoundTrip) { + Lcg rng(0xBEEF); + for (int trial = 0; trial < 20; ++trial) { + ByteBuffer input; + int len = static_cast(rng.next() % 5000); + for (int i = 0; i < len; ++i) input.push_back(rng.byte(256)); + ByteBuffer encoded = lz77Compress(input.data(), input.size()); + ByteBuffer decoded; + CHECK(lz77Decompress(encoded.data(), encoded.size(), decoded)); + CHECK(decoded == input); + } +} + +VF_TEST(lz77EmptyAndTiny) { + for (std::size_t n = 0; n < 8; ++n) { + ByteBuffer input(n, static_cast(n + 1)); + ByteBuffer encoded = lz77Compress(input.data(), input.size()); + ByteBuffer decoded; + CHECK(lz77Decompress(encoded.data(), encoded.size(), decoded)); + CHECK(decoded == input); + } +} + +VF_TEST(lz77RejectsCorruptStream) { + ByteBuffer input(500, 0x9); + ByteBuffer encoded = lz77Compress(input.data(), input.size()); + ByteBuffer decoded; + CHECK(lz77Decompress(encoded.data(), encoded.size(), decoded)); + ByteBuffer truncated(encoded.begin(), encoded.begin() + encoded.size() / 2); + ByteBuffer partial; + // A truncated stream must not silently claim success on a full reconstruction. + bool ok = lz77Decompress(truncated.data(), truncated.size(), partial); + CHECK(!(ok && partial == input)); +} + +VF_TEST(codecPicksBestAndRoundTrips) { + ByteBuffer runs(8000, 0x11); + ByteBuffer restored = roundTripCodec(runs); + CHECK(restored == runs); + ByteBuffer packed = compress(runs); + CHECK(static_cast(packed[0]) != CodecMethod::Store); + + Lcg rng(0x55AA); + ByteBuffer noise; + for (int i = 0; i < 4000; ++i) noise.push_back(rng.byte(256)); + ByteBuffer packedNoise = compress(noise); + CHECK(roundTripCodec(noise) == noise); + // Incompressible noise falls back to store, never larger than raw + header. + CHECK(packedNoise.size() <= noise.size() + 8); +} + +VF_TEST(codecDetectsChecksumTamper) { + ByteBuffer input; + for (int i = 0; i < 300; ++i) input.push_back(static_cast(i * 7)); + ByteBuffer packed = compress(input); + packed.back() ^= 0xFF; + ByteBuffer out; + bool ok = decompress(packed.data(), packed.size(), out); + CHECK(!(ok && out == input)); +} + +VF_TEST(checksumFunctionsAreStableAndDistinct) { + const uint8_t a[] = {1, 2, 3, 4, 5}; + const uint8_t b[] = {1, 2, 3, 4, 6}; + CHECK(fnv1a32(a, 5) == fnv1a32(a, 5)); + CHECK(fnv1a32(a, 5) != fnv1a32(b, 5)); + CHECK(fnv1a64(a, 5) != fnv1a64(b, 5)); + CHECK(adler32(a, 5) != adler32(b, 5)); + CHECK_EQ(adler32(nullptr, 0), 1u); +}