320c5e6ce5
assetLocaleGetString rewinds and linearly scans/re-decompresses the whole locale file from byte 0 on every single call, with no caching - a text-heavy screen can easily make 10+ of these in a row (e.g. opening the game menu), and on PSP the containing archive is already fully resident in RAM, so the repeated cost is pure CPU (decompression + scanning), not I/O. Adds a fixed 128-entry move-to-front LRU cache keyed by (messageId, pluralCount), capped at 64/256 bytes per key/value (~40KB total) so the cost stays bounded no matter how large the game's script ends up being, rather than caching the whole locale file's text. The cache is a lazily-allocated pointer on assetlocalefile_t, not embedded inline - that struct lives inside the assetloaderoutput_t union shared by every asset type, and all ASSET_ENTRY_COUNT_MAX asset slots carry that union directly, so embedding it would have sized every slot up by ~40KB regardless of what asset type occupies it. Also fixes a bug this surfaced in test_assetlocale.c's own fixture: locale_teardown zeroed the locale struct directly instead of going through assetLocaleDispose, which would have leaked the new cache allocation across tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
709 lines
24 KiB
C
709 lines
24 KiB
C
/**
|
|
* Copyright (c) 2026 Dominic Masters
|
|
*
|
|
* This software is released under the MIT License.
|
|
* https://opensource.org/licenses/MIT
|
|
*/
|
|
|
|
#include "dusktest.h"
|
|
#include "asset/loader/locale/assetlocaleloader.h"
|
|
#include "asset/asset.h"
|
|
#include "util/memory.h"
|
|
#include "util/string.h"
|
|
#include <zip.h>
|
|
|
|
// ============================================================
|
|
// Test locale file (gettext PO format)
|
|
// ============================================================
|
|
|
|
static const char_t *LOCALE_EN =
|
|
"msgid \"\"\n"
|
|
"msgstr \"\"\n"
|
|
"\"Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\\n\"\n"
|
|
"\n"
|
|
"msgid \"greeting\"\n"
|
|
"msgstr \"Hello, World!\"\n"
|
|
"\n"
|
|
"msgid \"item\"\n"
|
|
"msgid_plural \"items\"\n"
|
|
"msgstr[0] \"one item\"\n"
|
|
"msgstr[1] \"many items\"\n"
|
|
"\n"
|
|
"msgid \"score\"\n"
|
|
"msgstr \"Score: %d\"\n"
|
|
"\n"
|
|
"msgid \"player\"\n"
|
|
"msgstr \"Player: %s\"\n";
|
|
|
|
// ============================================================
|
|
// In-memory ZIP fixture (shared across all ZIP-based tests)
|
|
// ============================================================
|
|
|
|
static zip_t *g_zip = NULL;
|
|
static assetlocalefile_t g_locale;
|
|
|
|
static int locale_setup(void **state) {
|
|
zip_error_t err;
|
|
zip_error_init(&err);
|
|
|
|
// Phase 1: write the zip to a growable buffer source
|
|
zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err);
|
|
if(!write_src) return -1;
|
|
|
|
zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err);
|
|
if(!za) { zip_source_free(write_src); return -1; }
|
|
|
|
size_t flen = strlen(LOCALE_EN);
|
|
zip_source_t *fs = zip_source_buffer(za, LOCALE_EN, flen, 0);
|
|
if(zip_file_add(za, "en.locale", fs, ZIP_FL_OVERWRITE) < 0) {
|
|
zip_close(za); return -1;
|
|
}
|
|
|
|
// Keep write_src alive after zip_close so we can read the bytes back out
|
|
zip_source_keep(write_src);
|
|
if(zip_close(za) != 0) { zip_source_free(write_src); return -1; }
|
|
|
|
// Phase 2: extract the raw zip bytes from the write buffer.
|
|
// zip_source_stat must be called before zip_source_open on a written source.
|
|
zip_stat_t zs;
|
|
memset(&zs, 0, sizeof(zs));
|
|
if(zip_source_stat(write_src, &zs) != 0 || !(zs.valid & ZIP_STAT_SIZE)) {
|
|
zip_source_free(write_src); return -1;
|
|
}
|
|
|
|
void *zipbuf = malloc((size_t)zs.size);
|
|
if(!zipbuf) { zip_source_free(write_src); return -1; }
|
|
|
|
if(zip_source_open(write_src) != 0) {
|
|
free(zipbuf); zip_source_free(write_src); return -1;
|
|
}
|
|
zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size);
|
|
zip_source_close(write_src);
|
|
zip_source_free(write_src);
|
|
|
|
// Phase 3: open a fresh read-only archive from the extracted bytes.
|
|
// The archive takes ownership of the source (and thus zipbuf via freep=1).
|
|
zip_error_init(&err);
|
|
zip_source_t *read_src = zip_source_buffer_create(
|
|
zipbuf, (zip_uint64_t)zs.size, 1, &err
|
|
);
|
|
if(!read_src) { free(zipbuf); return -1; }
|
|
|
|
g_zip = zip_open_from_source(read_src, 0, &err);
|
|
if(!g_zip) { zip_source_free(read_src); return -1; }
|
|
|
|
ASSET.zip = g_zip;
|
|
|
|
// Init locale file and parse the header
|
|
memoryZero(&g_locale, sizeof(g_locale));
|
|
errorret_t ret = assetFileInit(&g_locale.file, "en.locale", NULL, NULL);
|
|
if(errorIsNotOk(ret)) { errorCatch(ret); goto fail; }
|
|
|
|
ret = assetFileOpen(&g_locale.file);
|
|
if(errorIsNotOk(ret)) { errorCatch(ret); goto fail; }
|
|
|
|
char_t header[512];
|
|
ret = assetLocaleGetString(&g_locale, "", 0, header, sizeof(header));
|
|
if(errorIsNotOk(ret)) { errorCatch(ret); assetFileClose(&g_locale.file); goto fail; }
|
|
|
|
ret = assetLocaleParseHeader(&g_locale, header, sizeof(header));
|
|
if(errorIsNotOk(ret)) { errorCatch(ret); assetFileClose(&g_locale.file); goto fail; }
|
|
|
|
return 0;
|
|
|
|
fail:
|
|
zip_close(g_zip); g_zip = NULL;
|
|
ASSET.zip = NULL;
|
|
return -1;
|
|
}
|
|
|
|
static int locale_teardown(void **state) {
|
|
if(g_locale.file.zipFile != NULL) {
|
|
errorret_t ret = assetFileClose(&g_locale.file);
|
|
if(errorIsNotOk(ret)) errorCatch(ret);
|
|
}
|
|
|
|
if(g_zip != NULL) {
|
|
zip_close(g_zip); // also frees the read_src and zipbuf
|
|
g_zip = NULL;
|
|
}
|
|
|
|
ASSET.zip = NULL;
|
|
if(g_locale.cache != NULL) memoryFree(g_locale.cache);
|
|
memoryZero(&g_locale, sizeof(g_locale));
|
|
return 0;
|
|
}
|
|
|
|
// ============================================================
|
|
// assetLocaleParseHeader - pure tests (no ZIP required)
|
|
// ============================================================
|
|
|
|
static void test_parseHeader_english(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n";
|
|
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_int_equal(locale.pluralStateCount, 2);
|
|
assert_int_equal(locale.pluralDefaultIndex, 0);
|
|
assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_NOT_EQUAL);
|
|
assert_int_equal(locale.pluralValues[0], 1);
|
|
assert_int_equal(locale.pluralIndices[0], 1);
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_parseHeader_singular(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t hdr[] = "Plural-Forms: nplurals=1; plural=(0);\n";
|
|
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_int_equal(locale.pluralStateCount, 1);
|
|
assert_int_equal(locale.pluralDefaultIndex, 0);
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_parseHeader_less_than(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n < 2 ? 0 : 1);\n";
|
|
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_int_equal(locale.pluralStateCount, 2);
|
|
assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_LESS);
|
|
assert_int_equal(locale.pluralValues[0], 2);
|
|
assert_int_equal(locale.pluralIndices[0], 0);
|
|
assert_int_equal(locale.pluralDefaultIndex, 1);
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_parseHeader_greater_equal(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n >= 2 ? 1 : 0);\n";
|
|
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_int_equal(locale.pluralStateCount, 2);
|
|
assert_int_equal(locale.pluralOps[0], ASSET_LOCALE_PLURAL_OP_GREATER_EQUAL);
|
|
assert_int_equal(locale.pluralValues[0], 2);
|
|
assert_int_equal(locale.pluralIndices[0], 1);
|
|
assert_int_equal(locale.pluralDefaultIndex, 0);
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_parseHeader_no_plural_forms(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t hdr[] = "Content-Type: text/plain; charset=UTF-8\n";
|
|
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_int_equal(locale.pluralStateCount, 0);
|
|
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_parseHeader_error_nplurals_zero(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t hdr[] = "Plural-Forms: nplurals=0; plural=(0);\n";
|
|
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_true(errorIsNotOk(ret));
|
|
errorCatch(ret);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_parseHeader_error_nplurals_too_large(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t hdr[] = "Plural-Forms: nplurals=7; plural=(0);\n";
|
|
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_true(errorIsNotOk(ret));
|
|
errorCatch(ret);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_parseHeader_error_missing_nplurals(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t hdr[] = "Plural-Forms: plural=(0);\n";
|
|
errorret_t ret = assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_true(errorIsNotOk(ret));
|
|
errorCatch(ret);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
// ============================================================
|
|
// assetLocaleEvaluatePlural - pure tests
|
|
// ============================================================
|
|
|
|
static void test_evaluatePlural_english_singular(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n";
|
|
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_evaluatePlural_english_plural(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n != 1 ? 1 : 0);\n";
|
|
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 1);
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 2), 1);
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 100), 1);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_evaluatePlural_singular_only(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
char_t hdr[] = "Plural-Forms: nplurals=1; plural=(0);\n";
|
|
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 0);
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0);
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 99), 0);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_evaluatePlural_less_than_boundary(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
char_t hdr[] = "Plural-Forms: nplurals=2; plural=(n < 2 ? 0 : 1);\n";
|
|
assetLocaleParseHeader(&locale, hdr, sizeof(hdr));
|
|
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 0), 0);
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 1), 0);
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 2), 1);
|
|
assert_int_equal(assetLocaleEvaluatePlural(&locale, 10), 1);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
// ============================================================
|
|
// assetLocaleGetString - ZIP-based tests
|
|
// ============================================================
|
|
|
|
static void test_getString_simple(void **state) {
|
|
char_t result[256];
|
|
errorret_t ret = assetLocaleGetString(&g_locale, "greeting", 0, result, sizeof(result));
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_string_equal(result, "Hello, World!");
|
|
// 1, not 0: the locale-string cache is lazily allocated on first use (the
|
|
// header lookup in locale_setup already triggered it) and lives for the
|
|
// rest of this file's lifetime - it's a bounded one-time cost, not a leak.
|
|
assert_int_equal(memoryGetAllocatedCount(), 1);
|
|
}
|
|
|
|
static void test_getString_plural_singular(void **state) {
|
|
char_t result[256];
|
|
errorret_t ret = assetLocaleGetString(&g_locale, "item", 1, result, sizeof(result));
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_string_equal(result, "one item");
|
|
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
|
}
|
|
|
|
static void test_getString_plural_many(void **state) {
|
|
char_t result[256];
|
|
errorret_t ret = assetLocaleGetString(&g_locale, "item", 5, result, sizeof(result));
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_string_equal(result, "many items");
|
|
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
|
}
|
|
|
|
static void test_getString_multiple_calls(void **state) {
|
|
char_t a[256], b[256];
|
|
errorret_t ret = assetLocaleGetString(&g_locale, "greeting", 0, a, sizeof(a));
|
|
assert_true(errorIsOk(ret));
|
|
|
|
// Second call is now served straight from the cache the first call
|
|
// populated, rather than rewinding the file and re-scanning from scratch.
|
|
ret = assetLocaleGetString(&g_locale, "greeting", 0, b, sizeof(b));
|
|
assert_true(errorIsOk(ret));
|
|
|
|
assert_string_equal(a, b);
|
|
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
|
}
|
|
|
|
static void test_getString_missing_id(void **state) {
|
|
char_t result[256];
|
|
errorret_t ret = assetLocaleGetString(&g_locale, "nonexistent", 0, result, sizeof(result));
|
|
|
|
assert_true(errorIsNotOk(ret));
|
|
errorCatch(ret);
|
|
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
|
}
|
|
|
|
// ============================================================
|
|
// assetLocaleCacheFind / assetLocaleCacheInsert - pure tests
|
|
// ============================================================
|
|
// These call the cache directly rather than through assetLocaleGetString -
|
|
// no open file is ever needed since the cache never touches file->file.
|
|
|
|
static void test_cache_findMiss_onEmptyCache(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t result[64];
|
|
bool_t hit = true;
|
|
errorret_t ret = assetLocaleCacheFind(&locale, "greeting", 0, result, sizeof(result), &hit);
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_false(hit);
|
|
assert_null(locale.cache);// a pure miss must not allocate anything
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_cache_insertThenFind_hits(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
assetLocaleCacheInsert(&locale, "greeting", 0, "Hello, World!");
|
|
assert_non_null(locale.cache);
|
|
|
|
char_t result[64];
|
|
bool_t hit = false;
|
|
errorret_t ret = assetLocaleCacheFind(&locale, "greeting", 0, result, sizeof(result), &hit);
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(hit);
|
|
assert_string_equal(result, "Hello, World!");
|
|
|
|
memoryFree(locale.cache);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_cache_distinguishesByPluralCount(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
assetLocaleCacheInsert(&locale, "item", 1, "one item");
|
|
assetLocaleCacheInsert(&locale, "item", 5, "many items");
|
|
|
|
char_t result[64];
|
|
bool_t hit = false;
|
|
|
|
errorret_t ret = assetLocaleCacheFind(&locale, "item", 1, result, sizeof(result), &hit);
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(hit);
|
|
assert_string_equal(result, "one item");
|
|
|
|
ret = assetLocaleCacheFind(&locale, "item", 5, result, sizeof(result), &hit);
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(hit);
|
|
assert_string_equal(result, "many items");
|
|
|
|
memoryFree(locale.cache);
|
|
}
|
|
|
|
static void test_cache_find_missOnDifferentMessageId(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
assetLocaleCacheInsert(&locale, "greeting", 0, "Hello, World!");
|
|
|
|
char_t result[64];
|
|
bool_t hit = true;
|
|
errorret_t ret = assetLocaleCacheFind(&locale, "farewell", 0, result, sizeof(result), &hit);
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_false(hit);
|
|
|
|
memoryFree(locale.cache);
|
|
}
|
|
|
|
static void test_cache_find_bufferTooSmall_errors(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
assetLocaleCacheInsert(&locale, "greeting", 0, "Hello, World!");
|
|
|
|
char_t result[4];// too small for "Hello, World!"
|
|
bool_t hit = false;
|
|
errorret_t ret = assetLocaleCacheFind(&locale, "greeting", 0, result, sizeof(result), &hit);
|
|
|
|
assert_true(errorIsNotOk(ret));
|
|
errorCatch(ret);
|
|
|
|
memoryFree(locale.cache);
|
|
}
|
|
|
|
static void test_cache_insert_tooLongMessageId_notCached(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t longId[ASSET_LOCALE_CACHE_KEY_MAX + 1];
|
|
memorySet(longId, 'a', sizeof(longId) - 1);
|
|
longId[sizeof(longId) - 1] = '\0';
|
|
|
|
assetLocaleCacheInsert(&locale, longId, 0, "value");
|
|
|
|
// Nothing should have been cached - the cache itself is never even
|
|
// allocated, since this was the only insert attempted.
|
|
assert_null(locale.cache);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_cache_insert_tooLongValue_notCached(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
char_t longValue[ASSET_LOCALE_CACHE_VALUE_MAX + 1];
|
|
memorySet(longValue, 'a', sizeof(longValue) - 1);
|
|
longValue[sizeof(longValue) - 1] = '\0';
|
|
|
|
assetLocaleCacheInsert(&locale, "greeting", 0, longValue);
|
|
|
|
assert_null(locale.cache);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
}
|
|
|
|
static void test_cache_insert_evictsLeastRecentlyUsed(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
// Fill the cache completely, "id0" first (so it's the first to become
|
|
// least-recently-used) through "id127" last (most-recently-used).
|
|
char_t key[32], value[32];
|
|
for(int32_t i = 0; i < ASSET_LOCALE_CACHE_COUNT; i++) {
|
|
stringFormat(key, sizeof(key), "id%d", i);
|
|
stringFormat(value, sizeof(value), "value%d", i);
|
|
assetLocaleCacheInsert(&locale, key, 0, value);
|
|
}
|
|
|
|
// One more insert should evict "id0", the least-recently-used entry.
|
|
assetLocaleCacheInsert(&locale, "newcomer", 0, "new value");
|
|
|
|
char_t result[32];
|
|
bool_t hit = true;
|
|
errorret_t ret = assetLocaleCacheFind(&locale, "id0", 0, result, sizeof(result), &hit);
|
|
assert_true(errorIsOk(ret));
|
|
assert_false(hit);
|
|
|
|
// But "id1" (and everything after it) should have survived.
|
|
ret = assetLocaleCacheFind(&locale, "id1", 0, result, sizeof(result), &hit);
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(hit);
|
|
assert_string_equal(result, "value1");
|
|
|
|
memoryFree(locale.cache);
|
|
}
|
|
|
|
static void test_cache_find_movesHitToFront(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
|
|
// Fill the cache, then immediately re-touch "id0" so it becomes
|
|
// most-recently-used instead of least-recently-used.
|
|
char_t key[32], value[32];
|
|
for(int32_t i = 0; i < ASSET_LOCALE_CACHE_COUNT; i++) {
|
|
stringFormat(key, sizeof(key), "id%d", i);
|
|
stringFormat(value, sizeof(value), "value%d", i);
|
|
assetLocaleCacheInsert(&locale, key, 0, value);
|
|
}
|
|
|
|
char_t result[32];
|
|
bool_t hit = false;
|
|
errorret_t ret = assetLocaleCacheFind(&locale, "id0", 0, result, sizeof(result), &hit);
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(hit);
|
|
|
|
// Without the touch above, this insert would have evicted "id0" (the
|
|
// original least-recently-used entry) - it should now evict "id1" instead.
|
|
assetLocaleCacheInsert(&locale, "newcomer", 0, "new value");
|
|
|
|
ret = assetLocaleCacheFind(&locale, "id0", 0, result, sizeof(result), &hit);
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(hit);
|
|
|
|
ret = assetLocaleCacheFind(&locale, "id1", 0, result, sizeof(result), &hit);
|
|
assert_true(errorIsOk(ret));
|
|
assert_false(hit);
|
|
|
|
memoryFree(locale.cache);
|
|
}
|
|
|
|
static void test_cache_nullAsserts(void **state) {
|
|
assetlocalefile_t locale;
|
|
memoryZero(&locale, sizeof(locale));
|
|
char_t result[64];
|
|
bool_t hit;
|
|
|
|
expect_assert_failure(
|
|
assetLocaleCacheFind(NULL, "greeting", 0, result, sizeof(result), &hit)
|
|
);
|
|
expect_assert_failure(
|
|
assetLocaleCacheFind(&locale, NULL, 0, result, sizeof(result), &hit)
|
|
);
|
|
expect_assert_failure(
|
|
assetLocaleCacheFind(&locale, "greeting", 0, result, sizeof(result), NULL)
|
|
);
|
|
|
|
expect_assert_failure(assetLocaleCacheInsert(NULL, "greeting", 0, "value"));
|
|
expect_assert_failure(assetLocaleCacheInsert(&locale, NULL, 0, "value"));
|
|
expect_assert_failure(assetLocaleCacheInsert(&locale, "greeting", 0, NULL));
|
|
}
|
|
|
|
static void test_localeDispose_freesCache(void **state) {
|
|
zip_error_t err;
|
|
zip_error_init(&err);
|
|
zip_source_t *write_src = zip_source_buffer_create(NULL, 0, 1, &err);
|
|
assert_non_null(write_src);
|
|
zip_t *za = zip_open_from_source(write_src, ZIP_TRUNCATE, &err);
|
|
assert_non_null(za);
|
|
size_t flen = strlen(LOCALE_EN);
|
|
zip_source_t *fs = zip_source_buffer(za, LOCALE_EN, flen, 0);
|
|
assert_true(zip_file_add(za, "en.locale", fs, ZIP_FL_OVERWRITE) >= 0);
|
|
zip_source_keep(write_src);
|
|
assert_int_equal(zip_close(za), 0);
|
|
|
|
zip_stat_t zs;
|
|
memset(&zs, 0, sizeof(zs));
|
|
assert_int_equal(zip_source_stat(write_src, &zs), 0);
|
|
void *zipbuf = malloc((size_t)zs.size);
|
|
assert_non_null(zipbuf);
|
|
assert_int_equal(zip_source_open(write_src), 0);
|
|
zip_source_read(write_src, zipbuf, (zip_uint64_t)zs.size);
|
|
zip_source_close(write_src);
|
|
zip_source_free(write_src);
|
|
|
|
zip_error_init(&err);
|
|
zip_source_t *read_src = zip_source_buffer_create(zipbuf, (zip_uint64_t)zs.size, 1, &err);
|
|
assert_non_null(read_src);
|
|
zip_t *zip = zip_open_from_source(read_src, 0, &err);
|
|
assert_non_null(zip);
|
|
ASSET.zip = zip;
|
|
|
|
assetentry_t entry;
|
|
memoryZero(&entry, sizeof(entry));
|
|
entry.type = ASSET_LOADER_TYPE_LOCALE;
|
|
|
|
errorret_t ret = assetFileInit(&entry.data.locale.file, "en.locale", NULL, NULL);
|
|
assert_true(errorIsOk(ret));
|
|
ret = assetFileOpen(&entry.data.locale.file);
|
|
assert_true(errorIsOk(ret));
|
|
|
|
char_t header[512];
|
|
ret = assetLocaleGetString(&entry.data.locale, "", 0, header, sizeof(header));
|
|
assert_true(errorIsOk(ret));
|
|
assert_non_null(entry.data.locale.cache);// the header lookup populated it
|
|
|
|
ret = assetLocaleDispose(&entry);
|
|
assert_true(errorIsOk(ret));
|
|
assert_null(entry.data.locale.cache);
|
|
assert_int_equal(memoryGetAllocatedCount(), 0);
|
|
|
|
zip_close(zip);
|
|
ASSET.zip = NULL;
|
|
}
|
|
|
|
// ============================================================
|
|
// assetLocaleGetStringWithArgs - ZIP-based tests
|
|
// ============================================================
|
|
|
|
static void test_getStringWithArgs_int(void **state) {
|
|
assetlocalearg_t args[] = {
|
|
{ .type = ASSET_LOCALE_ARG_INT, .intValue = 42 }
|
|
};
|
|
char_t result[256];
|
|
errorret_t ret = assetLocaleGetStringWithArgs(
|
|
&g_locale, "score", 0, result, sizeof(result), args, 1
|
|
);
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_string_equal(result, "Score: 42");
|
|
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
|
}
|
|
|
|
static void test_getStringWithArgs_string(void **state) {
|
|
assetlocalearg_t args[] = {
|
|
{ .type = ASSET_LOCALE_ARG_STRING, .stringValue = "Alice" }
|
|
};
|
|
char_t result[256];
|
|
errorret_t ret = assetLocaleGetStringWithArgs(
|
|
&g_locale, "player", 0, result, sizeof(result), args, 1
|
|
);
|
|
|
|
assert_true(errorIsOk(ret));
|
|
assert_string_equal(result, "Player: Alice");
|
|
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
|
}
|
|
|
|
// ============================================================
|
|
// main
|
|
// ============================================================
|
|
|
|
int main(void) {
|
|
assertInit();
|
|
const struct CMUnitTest tests[] = {
|
|
// parseHeader - pure
|
|
cmocka_unit_test(test_parseHeader_english),
|
|
cmocka_unit_test(test_parseHeader_singular),
|
|
cmocka_unit_test(test_parseHeader_less_than),
|
|
cmocka_unit_test(test_parseHeader_greater_equal),
|
|
cmocka_unit_test(test_parseHeader_no_plural_forms),
|
|
cmocka_unit_test(test_parseHeader_error_nplurals_zero),
|
|
cmocka_unit_test(test_parseHeader_error_nplurals_too_large),
|
|
cmocka_unit_test(test_parseHeader_error_missing_nplurals),
|
|
|
|
// evaluatePlural - pure
|
|
cmocka_unit_test(test_evaluatePlural_english_singular),
|
|
cmocka_unit_test(test_evaluatePlural_english_plural),
|
|
cmocka_unit_test(test_evaluatePlural_singular_only),
|
|
cmocka_unit_test(test_evaluatePlural_less_than_boundary),
|
|
|
|
// getString - in-memory ZIP
|
|
cmocka_unit_test_setup_teardown(test_getString_simple, locale_setup, locale_teardown),
|
|
cmocka_unit_test_setup_teardown(test_getString_plural_singular, locale_setup, locale_teardown),
|
|
cmocka_unit_test_setup_teardown(test_getString_plural_many, locale_setup, locale_teardown),
|
|
cmocka_unit_test_setup_teardown(test_getString_multiple_calls, locale_setup, locale_teardown),
|
|
cmocka_unit_test_setup_teardown(test_getString_missing_id, locale_setup, locale_teardown),
|
|
|
|
// getStringWithArgs - in-memory ZIP
|
|
cmocka_unit_test_setup_teardown(test_getStringWithArgs_int, locale_setup, locale_teardown),
|
|
cmocka_unit_test_setup_teardown(test_getStringWithArgs_string, locale_setup, locale_teardown),
|
|
|
|
// assetLocaleCacheFind / assetLocaleCacheInsert - pure
|
|
cmocka_unit_test(test_cache_findMiss_onEmptyCache),
|
|
cmocka_unit_test(test_cache_insertThenFind_hits),
|
|
cmocka_unit_test(test_cache_distinguishesByPluralCount),
|
|
cmocka_unit_test(test_cache_find_missOnDifferentMessageId),
|
|
cmocka_unit_test(test_cache_find_bufferTooSmall_errors),
|
|
cmocka_unit_test(test_cache_insert_tooLongMessageId_notCached),
|
|
cmocka_unit_test(test_cache_insert_tooLongValue_notCached),
|
|
cmocka_unit_test(test_cache_insert_evictsLeastRecentlyUsed),
|
|
cmocka_unit_test(test_cache_find_movesHitToFront),
|
|
cmocka_unit_test(test_cache_nullAsserts),
|
|
|
|
// assetLocaleDispose - in-memory ZIP
|
|
cmocka_unit_test(test_localeDispose_freesCache),
|
|
};
|
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
|
}
|