YAPF/tests/test_yapf.c
Nexoniarz d7f494a0c9 Language bindings, KDE thumbnails, yapf compare, in-memory encoding
- yapf_encode() / yapf_free_buffer(): encode to memory; yapf_save uses it
- yapf.c also compiles as C++
- `yapf compare`: size and decode speed of the original format vs YAPF,
  with a lossless check
- bindings with tested examples: C++ (header-only), C# (P/Invoke),
  Rust (crate, builds the C code), Go (cgo), Java (FFM API, Java 22+),
  Zig (@cImport), LuaJIT (FFI), TypeScript types for the JS port;
  Python now encodes in memory
- docs/LANGUAGES.md and examples/ with measured results
- KDE Dolphin thumbnail plugin (KF6, CMake)
- README: honest status for Photoshop / VS Code / Figma (not run in the
  apps yet) and macOS (cross-compiled only), platform table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:39:56 +02:00

202 lines
7.2 KiB
C

/*
* test_yapf.c — self-contained test suite for yapf.c
*
* cc -O2 -std=c99 tests/test_yapf.c yapf.c -o test_yapf -pthread
* ./test_yapf [path/to/other/YAPF.YAPF]
*
* Round-trips many sizes, channel counts and image kinds, checks mip
* chains, single- vs multi-threaded decode, the bundled sample file, and
* that corrupted or truncated files are rejected without crashing.
* Exit code 0 means every test passed.
*/
#include "../yapf.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* FNV-1a of the decoded pixels of other/YAPF.YAPF (the YAPF logo). */
#define SAMPLE_HASH 0x8891c2b4u
static int tests, fails;
static uint32_t rng = 12345;
static uint32_t rnd(void) { rng = rng * 1103515245u + 12345u; return rng >> 8; }
#define CHECK(cond, ...) do { tests++; if (!(cond)) { fails++; \
printf("FAIL: "); printf(__VA_ARGS__); printf("\n"); } } while (0)
/* Five kinds of content: noise, smooth gradient, hard-edged blocks,
* noisy curves, and an image with an opaque alpha channel. */
static void fill(uint8_t *p, int w, int h, int ch, int kind) {
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
for (int c = 0; c < ch; c++) {
uint8_t *d = p + ((size_t)y * w + x) * ch + c;
switch (kind) {
case 0: *d = (uint8_t)rnd(); break;
case 1: *d = (uint8_t)(x * 3 + y * 5 + c * 40); break;
case 2: *d = ((x / 7 + y / 5 + c) & 1) ? 200 : 30; break;
case 3: *d = (uint8_t)((x * x + y * y) / 50 + rnd() % 5); break;
default: *d = (c == ch - 1) ? 255 : (uint8_t)(x ^ y); break;
}
}
}
static uint8_t *encode_mem(const yapf_image_t *img, size_t *n) {
void *data = NULL;
if (yapf_encode(img, &data, n) != YAPF_OK) return NULL;
uint8_t *copy = (uint8_t *)malloc(*n);
if (copy) memcpy(copy, data, *n);
yapf_free_buffer(data);
return copy;
}
/* yapf_save() writes exactly what yapf_encode() returns. */
static void save_matches_encode(void) {
const char *tmp = "test_yapf_tmp.yapf";
uint8_t px[37 * 23 * 3];
fill(px, 37, 23, 3, 3);
yapf_image_t img;
memset(&img, 0, sizeof(img));
img.width = 37; img.height = 23; img.channels = 3; img.mip_levels = 1; img.pixels = px;
size_t len = 0;
uint8_t *mem = encode_mem(&img, &len);
int ok = mem && yapf_save(tmp, &img) == YAPF_OK;
FILE *f = ok ? fopen(tmp, "rb") : NULL;
if (f) {
uint8_t *disk = (uint8_t *)malloc(len + 1);
ok = fread(disk, 1, len + 1, f) == len && memcmp(disk, mem, len) == 0;
free(disk);
fclose(f);
}
remove(tmp);
free(mem);
CHECK(ok, "yapf_save writes the same bytes as yapf_encode");
void *d = (void *)1;
size_t n = 1;
CHECK(yapf_encode(NULL, &d, &n) == YAPF_ERR_INVALID && d == NULL && n == 0,
"yapf_encode rejects a NULL image");
}
static void roundtrip(int w, int h, int ch, int kind) {
size_t n = (size_t)w * h * ch, len = 0;
uint8_t *p = (uint8_t *)malloc(n);
fill(p, w, h, ch, kind);
yapf_image_t img;
memset(&img, 0, sizeof(img));
img.width = (uint32_t)w; img.height = (uint32_t)h;
img.channels = (uint8_t)ch; img.mip_levels = 1; img.pixels = p;
uint8_t *file = encode_mem(&img, &len);
CHECK(file != NULL, "encode %dx%d ch%d kind%d", w, h, ch, kind);
if (file) {
yapf_image_t *a = yapf_load_memory(file, len);
yapf_image_t *b = yapf_load_memory_mt(file, len, 0);
CHECK(a && memcmp(a->pixels, p, n) == 0,
"roundtrip %dx%d ch%d kind%d", w, h, ch, kind);
CHECK(b && memcmp(b->pixels, p, n) == 0,
"roundtrip (threads) %dx%d ch%d kind%d", w, h, ch, kind);
yapf_free(a);
yapf_free(b);
free(file);
}
free(p);
}
static void mips(void) {
const int w = 300, h = 200, levels = 9;
uint8_t *mp[9];
yapf_image_t img;
memset(&img, 0, sizeof(img));
img.width = w; img.height = h; img.channels = 4; img.mip_levels = levels;
for (int l = 0; l < levels; l++) {
int mw = w >> l ? w >> l : 1, mh = h >> l ? h >> l : 1;
mp[l] = (uint8_t *)malloc((size_t)mw * mh * 4);
fill(mp[l], mw, mh, 4, l % 5);
}
img.pixels = mp[0];
img.mips = mp;
size_t len = 0;
uint8_t *file = encode_mem(&img, &len);
yapf_image_t *d = file ? yapf_load_memory_mt(file, len, 3) : NULL;
int ok = d && d->mip_levels == levels;
for (int l = 0; ok && l < levels; l++) {
int mw = w >> l ? w >> l : 1, mh = h >> l ? h >> l : 1;
ok = memcmp(d->mips[l], mp[l], (size_t)mw * mh * 4) == 0;
}
CHECK(ok, "mip chain of %d levels", levels);
yapf_free(d);
free(file);
for (int l = 0; l < levels; l++) free(mp[l]);
}
/* Flip bits, truncate, scribble: the decoder must never crash, and a
* truncated file must be rejected. */
static void corruption(void) {
const int w = 130, h = 70, ch = 4;
uint8_t *p = (uint8_t *)malloc((size_t)w * h * ch);
fill(p, w, h, ch, 3);
yapf_image_t img;
memset(&img, 0, sizeof(img));
img.width = w; img.height = h; img.channels = ch; img.mip_levels = 1; img.pixels = p;
size_t len = 0;
uint8_t *file = encode_mem(&img, &len);
uint8_t *bad = (uint8_t *)malloc(len);
int truncated_ok = 1;
for (int it = 0; it < 3000 && file; it++) {
memcpy(bad, file, len);
size_t n = len;
switch (it % 3) {
case 0: for (int k = 0; k < 1 + it % 7; k++) bad[rnd() % len] ^= (uint8_t)(1u << (rnd() % 8)); break;
case 1: n = rnd() % len; break;
default: bad[rnd() % len] = (uint8_t)rnd(); break;
}
yapf_image_t *d = (it & 1) ? yapf_load_memory_mt(bad, n, 2) : yapf_load_memory(bad, n);
if (it % 3 == 1 && d) truncated_ok = 0;
yapf_free(d);
}
CHECK(file != NULL, "corruption test setup");
CHECK(truncated_ok, "truncated files are rejected");
free(bad);
free(file);
free(p);
}
static uint32_t fnv(const uint8_t *p, size_t n) {
uint32_t h = 2166136261u;
for (size_t i = 0; i < n; i++) h = (h ^ p[i]) * 16777619u;
return h;
}
int main(int argc, char **argv) {
static const int sizes[][2] = {
{1, 1}, {1, 7}, {7, 1}, {63, 63}, {64, 64}, {65, 65}, {127, 1},
{1, 129}, {97, 61}, {200, 130}, {256, 256}, {513, 77},
{1000, 700} /* 176 tiles: big enough for the threaded decoder */
};
for (size_t s = 0; s < sizeof(sizes) / sizeof(sizes[0]); s++)
for (int ch = 1; ch <= 4; ch++)
for (int kind = 0; kind < 5; kind++)
roundtrip(sizes[s][0], sizes[s][1], ch, kind);
mips();
corruption();
save_matches_encode();
const char *sample = argc > 1 ? argv[1] : "other/YAPF.YAPF";
yapf_image_t *img = yapf_load(sample);
if (img) {
uint32_t h = fnv(img->pixels, (size_t)img->width * img->height * img->channels);
CHECK(img->width == 512 && img->height == 512 && img->channels == 4 &&
h == SAMPLE_HASH, "sample %s decodes to the reference pixels (hash %08x)",
sample, (unsigned)h);
yapf_free(img);
} else {
printf("note: sample %s not found, skipped\n", sample);
}
printf("%d tests, %d failures\n", tests, fails);
return fails != 0;
}