Files
dusk/test/asset/test_assetlocale.c
T

424 lines
14 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 <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;
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!");
assert_int_equal(memoryGetAllocatedCount(), 0);
}
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(), 0);
}
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(), 0);
}
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 rewinds the file and re-reads 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);
}
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(), 0);
}
// ============================================================
// 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(), 0);
}
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(), 0);
}
// ============================================================
// main
// ============================================================
int main(void) {
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),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}