Cache locale string lookups instead of re-scanning the PO file every call
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>
This commit is contained in:
@@ -71,6 +71,12 @@ errorret_t assetLocaleDispose(assetentry_t *entry) {
|
||||
assertIsMainThread("Must be called from the main thread.");
|
||||
|
||||
assetlocalefile_t *localeFile = &entry->data.locale;
|
||||
|
||||
if(localeFile->cache != NULL) {
|
||||
memoryFree(localeFile->cache);
|
||||
localeFile->cache = NULL;
|
||||
}
|
||||
|
||||
errorChain(assetFileClose(&localeFile->file));
|
||||
return assetFileDispose(&localeFile->file);
|
||||
}
|
||||
@@ -470,6 +476,83 @@ errorret_t assetLocaleLineUnbuffer(
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t assetLocaleCacheFind(
|
||||
assetlocalefile_t *file,
|
||||
const char_t *messageId,
|
||||
const int32_t pluralCount,
|
||||
char_t *stringBuffer,
|
||||
const size_t stringBufferSize,
|
||||
bool_t *outHit
|
||||
) {
|
||||
assertNotNull(file, "Asset file cannot be NULL.");
|
||||
assertNotNull(messageId, "Message ID cannot be NULL.");
|
||||
assertNotNull(outHit, "outHit cannot be NULL.");
|
||||
|
||||
*outHit = false;
|
||||
if(file->cache == NULL) errorOk();
|
||||
|
||||
assetlocalecache_t *cache = file->cache;
|
||||
for(uint8_t i = 0; i < ASSET_LOCALE_CACHE_COUNT; i++) {
|
||||
assetlocalecacheentry_t *entry = &cache->entries[i];
|
||||
if(entry->messageId[0] == '\0') break;// unused tail - nothing further
|
||||
if(entry->pluralCount != pluralCount) continue;
|
||||
if(!stringEquals(entry->messageId, messageId)) continue;
|
||||
|
||||
// Move to the front of the LRU order (a no-op when already there).
|
||||
if(i > 0) {
|
||||
assetlocalecacheentry_t hit = *entry;
|
||||
memoryMove(
|
||||
&cache->entries[1], &cache->entries[0], i * sizeof(assetlocalecacheentry_t)
|
||||
);
|
||||
cache->entries[0] = hit;
|
||||
}
|
||||
|
||||
size_t len = strlen(cache->entries[0].value);
|
||||
if(len >= stringBufferSize) errorThrow("String buffer overflow");
|
||||
memoryCopy(stringBuffer, cache->entries[0].value, len + 1);
|
||||
*outHit = true;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void assetLocaleCacheInsert(
|
||||
assetlocalefile_t *file,
|
||||
const char_t *messageId,
|
||||
const int32_t pluralCount,
|
||||
const char_t *value
|
||||
) {
|
||||
assertNotNull(file, "Asset file cannot be NULL.");
|
||||
assertNotNull(messageId, "Message ID cannot be NULL.");
|
||||
assertNotNull(value, "Value cannot be NULL.");
|
||||
|
||||
// Caching is a pure optimization - a message ID or value too long for a
|
||||
// fixed cache slot is just never cached, not an error.
|
||||
if(strlen(messageId) >= ASSET_LOCALE_CACHE_KEY_MAX) return;
|
||||
if(strlen(value) >= ASSET_LOCALE_CACHE_VALUE_MAX) return;
|
||||
|
||||
if(file->cache == NULL) {
|
||||
file->cache = (assetlocalecache_t *)memoryAllocate(sizeof(assetlocalecache_t));
|
||||
memoryZero(file->cache, sizeof(assetlocalecache_t));
|
||||
}
|
||||
|
||||
// Shift every entry down one slot, dropping the least-recently-used one
|
||||
// off the end, to make room for the new entry at the front. This assumes
|
||||
// messageId is never already present elsewhere in the cache, which holds
|
||||
// as long as callers only insert after a confirmed assetLocaleCacheFind
|
||||
// miss (a hit would have returned before reaching this point).
|
||||
memoryMove(
|
||||
&file->cache->entries[1], &file->cache->entries[0],
|
||||
(ASSET_LOCALE_CACHE_COUNT - 1) * sizeof(assetlocalecacheentry_t)
|
||||
);
|
||||
|
||||
assetlocalecacheentry_t *entry = &file->cache->entries[0];
|
||||
stringCopy(entry->messageId, messageId, ASSET_LOCALE_CACHE_KEY_MAX);
|
||||
entry->pluralCount = pluralCount;
|
||||
stringCopy(entry->value, value, ASSET_LOCALE_CACHE_VALUE_MAX);
|
||||
}
|
||||
|
||||
errorret_t assetLocaleGetString(
|
||||
assetlocalefile_t *file,
|
||||
const char_t *messageId,
|
||||
@@ -482,6 +565,13 @@ errorret_t assetLocaleGetString(
|
||||
assertTrue(pluralCount >= 0, "Plural index cannot be negative.");
|
||||
assertNotNull(stringBuffer, "String buffer cannot be NULL.");
|
||||
assertTrue(stringBufferSize > 0, "String buffer size must be > 0");
|
||||
|
||||
bool_t cacheHit = false;
|
||||
errorChain(assetLocaleCacheFind(
|
||||
file, messageId, pluralCount, stringBuffer, stringBufferSize, &cacheHit
|
||||
));
|
||||
if(cacheHit) errorOk();
|
||||
|
||||
assetfilelinereader_t reader;
|
||||
|
||||
bool_t msgidFound = false, msgidPluralFound = false, msgstrFound = false;
|
||||
@@ -625,6 +715,8 @@ errorret_t assetLocaleGetString(
|
||||
errorThrow("Failed to find msgstr for message ID: %s", messageId);
|
||||
}
|
||||
|
||||
assetLocaleCacheInsert(file, messageId, pluralCount, stringBuffer);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,41 @@ typedef struct {
|
||||
};
|
||||
} assetlocalearg_t;
|
||||
|
||||
/** Number of recently resolved strings @ref assetlocalecache_t remembers. */
|
||||
#define ASSET_LOCALE_CACHE_COUNT 128
|
||||
|
||||
/** Max length (excluding null terminator) of a cacheable message ID. */
|
||||
#define ASSET_LOCALE_CACHE_KEY_MAX 64
|
||||
|
||||
/** Max length (excluding null terminator) of a cacheable resolved string. */
|
||||
#define ASSET_LOCALE_CACHE_VALUE_MAX 256
|
||||
|
||||
/** One (messageId, pluralCount) -> resolved string cache slot. */
|
||||
typedef struct {
|
||||
char_t messageId[ASSET_LOCALE_CACHE_KEY_MAX];
|
||||
int32_t pluralCount;
|
||||
char_t value[ASSET_LOCALE_CACHE_VALUE_MAX];
|
||||
} assetlocalecacheentry_t;
|
||||
|
||||
/**
|
||||
* Fixed-size move-to-front LRU cache of recently resolved locale strings.
|
||||
* assetLocaleGetString() re-scans and re-decompresses the whole PO file on
|
||||
* every miss, which is expensive to repeat for strings a screen fetches
|
||||
* every time it opens (menu labels, etc) - this cache lets identical
|
||||
* (messageId, pluralCount) lookups skip that entirely.
|
||||
*
|
||||
* Lazily allocated on the first cache insert (see assetlocalefile_t.cache)
|
||||
* so locale entries that are never queried, or a file that's disposed
|
||||
* before anything is cached, never pay for it.
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Entries ordered most-recently-used first. An entry with
|
||||
* messageId[0] == '\0' (and every entry after it) is unused.
|
||||
*/
|
||||
assetlocalecacheentry_t entries[ASSET_LOCALE_CACHE_COUNT];
|
||||
} assetlocalecache_t;
|
||||
|
||||
/**
|
||||
* Runtime state for an open locale file.
|
||||
*
|
||||
@@ -98,6 +133,16 @@ typedef struct {
|
||||
|
||||
/** Form index used when no conditional clause matches. */
|
||||
uint8_t pluralDefaultIndex;
|
||||
|
||||
/**
|
||||
* Recently resolved string cache, or NULL if nothing has been cached yet.
|
||||
* Heap-allocated rather than embedded because assetlocalefile_t lives
|
||||
* inside the shared assetloaderoutput_t union alongside every other
|
||||
* asset type - embedding a 128-entry cache there would size that union
|
||||
* (and therefore every one of the ASSET_ENTRY_COUNT_MAX asset slots,
|
||||
* regardless of what type of asset occupies it) up by the same amount.
|
||||
*/
|
||||
assetlocalecache_t *cache;
|
||||
} assetlocalefile_t;
|
||||
|
||||
/** Convenience alias - the loaded output type of a locale asset entry. */
|
||||
@@ -203,12 +248,64 @@ errorret_t assetLocaleLineUnbuffer(
|
||||
const size_t stringBufferSize
|
||||
);
|
||||
|
||||
/**
|
||||
* Searches a locale file's cache for a previously resolved
|
||||
* (messageId, pluralCount) pair.
|
||||
*
|
||||
* On a hit, copies the cached value into `stringBuffer` (erroring if it
|
||||
* doesn't fit, same contract as @ref assetLocaleGetString) and moves the
|
||||
* entry to the front of the cache's move-to-front LRU order. On a miss,
|
||||
* `stringBuffer` is left untouched.
|
||||
*
|
||||
* @param file Locale file whose cache to search. Its cache may be NULL
|
||||
* (nothing cached yet), which is treated as a miss.
|
||||
* @param messageId Message ID to look up.
|
||||
* @param pluralCount Plural count the original lookup used.
|
||||
* @param stringBuffer Destination buffer, filled only on a hit.
|
||||
* @param stringBufferSize Capacity of `stringBuffer` in bytes.
|
||||
* @param outHit Set to true on a cache hit, false on a miss.
|
||||
* @return OK on success (hit or miss), error if a hit's cached value does
|
||||
* not fit `stringBuffer`.
|
||||
*/
|
||||
errorret_t assetLocaleCacheFind(
|
||||
assetlocalefile_t *file,
|
||||
const char_t *messageId,
|
||||
const int32_t pluralCount,
|
||||
char_t *stringBuffer,
|
||||
const size_t stringBufferSize,
|
||||
bool_t *outHit
|
||||
);
|
||||
|
||||
/**
|
||||
* Records a resolved (messageId, pluralCount) -> value string at the front
|
||||
* of the cache's move-to-front LRU order, evicting the least-recently-used
|
||||
* entry if the cache is already full. Lazily allocates the cache (see
|
||||
* assetlocalefile_t.cache) on the first call.
|
||||
*
|
||||
* Caching is a pure optimization: if `messageId` or `value` is too long to
|
||||
* fit the cache's fixed-size slots, this silently does nothing rather than
|
||||
* erroring or growing the cache.
|
||||
*
|
||||
* @param file Locale file whose cache to insert into.
|
||||
* @param messageId Message ID that was looked up.
|
||||
* @param pluralCount Plural count the lookup was made with.
|
||||
* @param value Resolved string to cache.
|
||||
*/
|
||||
void assetLocaleCacheInsert(
|
||||
assetlocalefile_t *file,
|
||||
const char_t *messageId,
|
||||
const int32_t pluralCount,
|
||||
const char_t *value
|
||||
);
|
||||
|
||||
/**
|
||||
* Looks up a translated string by message ID from the open locale file.
|
||||
*
|
||||
* Rewinds the file and scans from the beginning on every call. For plural
|
||||
* entries (`msgid_plural`) the `pluralCount` is evaluated against the loaded
|
||||
* plural rules to select the correct `msgstr[N]` form.
|
||||
* Checks the file's cache first (see @ref assetLocaleCacheFind); on a miss,
|
||||
* rewinds the file and scans from the beginning, then caches the result
|
||||
* (see @ref assetLocaleCacheInsert) before returning. For plural entries
|
||||
* (`msgid_plural`) the `pluralCount` is evaluated against the loaded plural
|
||||
* rules to select the correct `msgstr[N]` form.
|
||||
*
|
||||
* @param file Locale file to search. Must be open.
|
||||
* @param messageId PO message ID to find (`""` retrieves the header entry).
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
#include "asset/asset.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include <zip.h>
|
||||
|
||||
// ============================================================
|
||||
@@ -128,6 +129,7 @@ static int locale_teardown(void **state) {
|
||||
}
|
||||
|
||||
ASSET.zip = NULL;
|
||||
if(g_locale.cache != NULL) memoryFree(g_locale.cache);
|
||||
memoryZero(&g_locale, sizeof(g_locale));
|
||||
return 0;
|
||||
}
|
||||
@@ -311,7 +313,10 @@ static void test_getString_simple(void **state) {
|
||||
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_string_equal(result, "Hello, World!");
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
// 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) {
|
||||
@@ -320,7 +325,7 @@ static void test_getString_plural_singular(void **state) {
|
||||
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_string_equal(result, "one item");
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
||||
}
|
||||
|
||||
static void test_getString_plural_many(void **state) {
|
||||
@@ -329,7 +334,7 @@ static void test_getString_plural_many(void **state) {
|
||||
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_string_equal(result, "many items");
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
||||
}
|
||||
|
||||
static void test_getString_multiple_calls(void **state) {
|
||||
@@ -337,12 +342,13 @@ static void test_getString_multiple_calls(void **state) {
|
||||
errorret_t ret = assetLocaleGetString(&g_locale, "greeting", 0, a, sizeof(a));
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
// Second call rewinds the file and re-reads from scratch.
|
||||
// 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(), 0);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
||||
}
|
||||
|
||||
static void test_getString_missing_id(void **state) {
|
||||
@@ -351,9 +357,272 @@ static void test_getString_missing_id(void **state) {
|
||||
|
||||
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
|
||||
// ============================================================
|
||||
@@ -369,7 +638,7 @@ static void test_getStringWithArgs_int(void **state) {
|
||||
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_string_equal(result, "Score: 42");
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
||||
}
|
||||
|
||||
static void test_getStringWithArgs_string(void **state) {
|
||||
@@ -383,7 +652,7 @@ static void test_getStringWithArgs_string(void **state) {
|
||||
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_string_equal(result, "Player: Alice");
|
||||
assert_int_equal(memoryGetAllocatedCount(), 0);
|
||||
assert_int_equal(memoryGetAllocatedCount(), 1);// see test_getString_simple
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -391,6 +660,7 @@ static void test_getStringWithArgs_string(void **state) {
|
||||
// ============================================================
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
// parseHeader - pure
|
||||
cmocka_unit_test(test_parseHeader_english),
|
||||
@@ -418,6 +688,21 @@ int main(void) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user