Files
radiantwave/tests/test_fibonacci_scrambler.cpp
T
2026-08-07 14:51:41 -05:00

294 lines
9.7 KiB
C++

#include "scramble/fibonacci_scrambler.h"
#include <gtest/gtest.h>
#include <cstdint>
#include <unordered_map>
#include <vector>
namespace {
using radiantwave::scramble::ScrambleConfig;
using radiantwave::scramble::canvasSize;
using radiantwave::scramble::scramble;
using radiantwave::scramble::unscramble;
// W x H RGBA8888 image whose every pixel has a distinct 32-bit value, so a
// permutation can be verified by counting values. Pixel (x, y) gets the value
// y*width + x + 1; the +1 keeps every source value distinct from the
// transparent-black padding (0).
std::vector<uint8_t> uniqueImage(int width, int height)
{
std::vector<uint8_t> img(static_cast<size_t>(width) * height * 4);
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
const uint32_t v = static_cast<uint32_t>(y) * width + x + 1;
uint8_t *p = img.data() + (static_cast<size_t>(y) * width + x) * 4;
p[0] = static_cast<uint8_t>(v & 0xFF);
p[1] = static_cast<uint8_t>((v >> 8) & 0xFF);
p[2] = static_cast<uint8_t>((v >> 16) & 0xFF);
p[3] = static_cast<uint8_t>((v >> 24) & 0xFF);
}
}
return img;
}
bool roundTrips(int width, int height, int rounds, uint64_t seed)
{
const auto img = uniqueImage(width, height);
const int n = canvasSize(width, height);
std::vector<uint8_t> scrambled(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> recovered(static_cast<size_t>(width) * height * 4);
ScrambleConfig cfg;
cfg.rounds = rounds;
cfg.seed = seed;
if (!scramble(img.data(), scrambled.data(), width, height, cfg))
{
return false;
}
if (!unscramble(scrambled.data(), recovered.data(), width, height, cfg))
{
return false;
}
return recovered == img;
}
} // namespace
TEST(FibonacciScramblerTest, CanvasSizeIsMaxOfDimensions)
{
EXPECT_EQ(canvasSize(1, 1), 1);
EXPECT_EQ(canvasSize(8, 8), 8);
EXPECT_EQ(canvasSize(8, 4), 8);
EXPECT_EQ(canvasSize(4, 8), 8);
EXPECT_EQ(canvasSize(1, 8), 8);
EXPECT_EQ(canvasSize(8, 1), 8);
EXPECT_EQ(canvasSize(0, 8), 0);
EXPECT_EQ(canvasSize(8, 0), 0);
EXPECT_EQ(canvasSize(0, 0), 0);
EXPECT_EQ(canvasSize(-3, 8), 0);
}
TEST(FibonacciScramblerTest, RoundTripIsExactForSquareImages)
{
const int sizes[] = {1, 2, 3, 8, 32};
for (int n : sizes)
{
for (int t = 1; t <= 6; ++t)
{
EXPECT_TRUE(roundTrips(n, n, t, radiantwave::scramble::kDefaultSeed))
<< "N=" << n << " rounds=" << t;
}
}
}
TEST(FibonacciScramblerTest, RoundTripIsExactForWideAndTallImages)
{
const int dims[][2] = {{8, 4}, {4, 8}, {1, 8}, {8, 1}, {3, 17}, {31, 3}};
for (const auto &d : dims)
{
for (int t : {1, 4})
{
EXPECT_TRUE(roundTrips(d[0], d[1], t, radiantwave::scramble::kDefaultSeed))
<< "W=" << d[0] << " H=" << d[1] << " rounds=" << t;
}
}
}
TEST(FibonacciScramblerTest, RoundTripIsExactForGlyphShapedTiles)
{
// Punctuation glyphs are narrow or short, so N is max(W, H): these exercise
// the non-square padding path with glyph-like proportions (e.g. an
// apostrophe tile is 6 wide x 38 tall at 32px).
const int dims[][2] = {{1, 5}, {2, 7}, {6, 1}, {1, 1},
{17, 37}, {9, 37}, {37, 9}};
for (const auto &d : dims)
{
for (int t : {1, 4})
{
EXPECT_TRUE(roundTrips(d[0], d[1], t, radiantwave::scramble::kDefaultSeed))
<< "W=" << d[0] << " H=" << d[1] << " rounds=" << t;
}
}
}
TEST(FibonacciScramblerTest, ScrambleIsABijectionOnTheCanvas)
{
const int dims[][2] = {{5, 7}, {8, 8}, {3, 1}, {1, 4}};
for (const auto &d : dims)
{
const int width = d[0];
const int height = d[1];
const int n = canvasSize(width, height);
const auto img = uniqueImage(width, height);
std::vector<uint8_t> scrambled(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfg;
cfg.rounds = 3;
ASSERT_TRUE(scramble(img.data(), scrambled.data(), width, height, cfg));
std::unordered_map<uint32_t, int> counts;
for (const uint8_t *p = scrambled.data(); p != scrambled.data() + scrambled.size(); p += 4)
{
const uint32_t v = static_cast<uint32_t>(p[0]) |
(static_cast<uint32_t>(p[1]) << 8) |
(static_cast<uint32_t>(p[2]) << 16) |
(static_cast<uint32_t>(p[3]) << 24);
counts[v]++;
}
// Every source pixel value appears exactly once (the map restricted to
// the W x H region is injective)...
const size_t pixels = static_cast<size_t>(width) * height;
for (size_t i = 0; i < pixels; ++i)
{
const uint32_t v = static_cast<uint32_t>(i) + 1;
EXPECT_EQ(counts[v], 1)
<< "W=" << width << " H=" << height << " value=" << v;
}
// ...and the remaining canvas positions are transparent-black padding.
EXPECT_EQ(counts[0], static_cast<int>(static_cast<size_t>(n) * n - pixels))
<< "W=" << width << " H=" << height;
}
}
TEST(FibonacciScramblerTest, ScrambleChangesTheImage)
{
const auto img = uniqueImage(8, 8);
const int n = 8;
std::vector<uint8_t> scrambled(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfg;
cfg.rounds = 1;
ASSERT_TRUE(scramble(img.data(), scrambled.data(), 8, 8, cfg));
bool changed = false;
for (size_t i = 0; i < img.size(); ++i)
{
if (scrambled[i] != img[i])
{
changed = true;
break;
}
}
EXPECT_TRUE(changed);
}
TEST(FibonacciScramblerTest, OutputIsDeterministic)
{
const auto img = uniqueImage(6, 9);
const int n = 9;
std::vector<uint8_t> a(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> b(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfg;
cfg.rounds = 3;
ASSERT_TRUE(scramble(img.data(), a.data(), 6, 9, cfg));
ASSERT_TRUE(scramble(img.data(), b.data(), 6, 9, cfg));
EXPECT_EQ(a, b);
}
TEST(FibonacciScramblerTest, DifferentSeedChangesOutput)
{
const auto img = uniqueImage(8, 8);
const int n = 8;
std::vector<uint8_t> a(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> b(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfgA;
cfgA.rounds = 2;
cfgA.seed = radiantwave::scramble::kDefaultSeed;
ScrambleConfig cfgB = cfgA;
cfgB.seed = cfgA.seed + 1;
ASSERT_TRUE(scramble(img.data(), a.data(), 8, 8, cfgA));
ASSERT_TRUE(scramble(img.data(), b.data(), 8, 8, cfgB));
EXPECT_NE(a, b);
}
TEST(FibonacciScramblerTest, DifferentRoundsChangeOutput)
{
const auto img = uniqueImage(8, 8);
const int n = 8;
std::vector<uint8_t> a(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> b(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfgA;
cfgA.rounds = 1;
ScrambleConfig cfgB;
cfgB.rounds = 2;
ASSERT_TRUE(scramble(img.data(), a.data(), 8, 8, cfgA));
ASSERT_TRUE(scramble(img.data(), b.data(), 8, 8, cfgB));
EXPECT_NE(a, b);
}
TEST(FibonacciScramblerTest, AlphaChannelIsPreservedByRoundTrip)
{
// Pixel values exercise all 32 bits, alpha included.
EXPECT_TRUE(roundTrips(11, 7, 5, 12345));
}
TEST(FibonacciScramblerTest, PaddingIsTransparentAndRoundTrips)
{
// Opaque white source: after scrambling, every canvas pixel must be either
// white (a real source pixel) or transparent black (padding), and the
// unscramble must restore pure white.
const int width = 5;
const int height = 7;
const int n = canvasSize(width, height);
std::vector<uint8_t> img(static_cast<size_t>(width) * height * 4, 255);
std::vector<uint8_t> scrambled(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> recovered(static_cast<size_t>(width) * height * 4);
ScrambleConfig cfg;
cfg.rounds = 4;
ASSERT_TRUE(scramble(img.data(), scrambled.data(), width, height, cfg));
for (size_t i = 0; i < scrambled.size(); i += 4)
{
const bool isWhite = scrambled[i] == 255 && scrambled[i + 1] == 255 &&
scrambled[i + 2] == 255 && scrambled[i + 3] == 255;
const bool isPadding = scrambled[i] == 0 && scrambled[i + 1] == 0 &&
scrambled[i + 2] == 0 && scrambled[i + 3] == 0;
EXPECT_TRUE(isWhite || isPadding)
<< "canvas pixel " << (i / 4) << " is neither white nor padding";
}
ASSERT_TRUE(unscramble(scrambled.data(), recovered.data(), width, height, cfg));
EXPECT_EQ(recovered, img);
}
TEST(FibonacciScramblerTest, RoundsBelowOneClampToOne)
{
const auto img = uniqueImage(7, 5);
const int n = 7;
std::vector<uint8_t> clamped(static_cast<size_t>(n) * n * 4);
std::vector<uint8_t> single(static_cast<size_t>(n) * n * 4);
ScrambleConfig cfgClamped;
cfgClamped.rounds = 0;
ScrambleConfig cfgSingle;
cfgSingle.rounds = 1;
ASSERT_TRUE(scramble(img.data(), clamped.data(), 7, 5, cfgClamped));
ASSERT_TRUE(scramble(img.data(), single.data(), 7, 5, cfgSingle));
EXPECT_EQ(clamped, single);
EXPECT_TRUE(roundTrips(7, 5, 0, radiantwave::scramble::kDefaultSeed));
}
TEST(FibonacciScramblerTest, RejectsInvalidArguments)
{
std::vector<uint8_t> dummy(16 * 16 * 4, 0);
ScrambleConfig cfg;
EXPECT_FALSE(scramble(nullptr, dummy.data(), 4, 4, cfg));
EXPECT_FALSE(scramble(dummy.data(), nullptr, 4, 4, cfg));
EXPECT_FALSE(scramble(dummy.data(), dummy.data(), 0, 4, cfg));
EXPECT_FALSE(scramble(dummy.data(), dummy.data(), 4, -1, cfg));
EXPECT_FALSE(unscramble(nullptr, dummy.data(), 4, 4, cfg));
EXPECT_FALSE(unscramble(dummy.data(), nullptr, 4, 4, cfg));
EXPECT_FALSE(unscramble(dummy.data(), dummy.data(), 0, 4, cfg));
}