/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "dusktest.h" #include "save/saveslot.h" #include "save/savejson.h" #include "util/memory.h" #include "util/string.h" #include #include // ============================================================ // Helpers - wrap the writeInit/readInit macros (which can only be used // inside a function returning errorret_t) so tests can call them plainly. // ============================================================ static errorret_t slotToJSON(saveslot_t *slot, char_t **outJson, size_t *outLen) { writeInit(); errorret_t writeResult = saveSlotWriteJSON(slot, doc, object); if(errorIsNotOk(writeResult)) { yyjson_mut_doc_free(doc); errorChain(writeResult); } *outJson = yyjson_mut_write(doc, 0, outLen); yyjson_mut_doc_free(doc); assertNotNull(*outJson, "Failed to write slot JSON"); errorOk(); } static errorret_t slotFromJSON(saveslot_t *slot, const char_t *json) { readInit(json, strlen(json)); errorret_t readResult = saveSlotReadJSON(slot, object); yyjson_doc_free(jsonDoc); errorChain(readResult); errorOk(); } // ============================================================ // saveSlotInit // ============================================================ static void test_saveSlotInit_defaults(void **state) { saveslot_t slot; memorySet(&slot, 0xFF, sizeof(slot)); saveSlotInit(&slot); assert_int_equal(slot.version, 1); assert_int_equal(slot.dataType, 0); assert_int_equal(slot.cachedData.name[0], '\0'); assert_true(slot.cachedData.time.time == 0.0); assert_int_equal(slot.cachedData.playerLevel, 0); } static void test_saveSlotInit_nullAsserts(void **state) { expect_assert_failure(saveSlotInit(NULL)); } // ============================================================ // saveSlotInUse / saveSlotHasSaved // ============================================================ static void test_saveSlotInUse(void **state) { saveslot_t slot; saveSlotInit(&slot); assert_false(saveSlotInUse(&slot.cachedData)); stringCopy(slot.cachedData.name, "Hero", sizeof(slot.cachedData.name)); assert_true(saveSlotInUse(&slot.cachedData)); } static void test_saveSlotInUse_nullAsserts(void **state) { expect_assert_failure(saveSlotInUse(NULL)); } static void test_saveSlotHasSaved(void **state) { saveslot_t slot; saveSlotInit(&slot); assert_false(saveSlotHasSaved(&slot.cachedData)); // Writing to JSON stamps the current time as a side effect, so even a // pure serialize (no device write) flips "has ever saved" to true. char_t *json; size_t len; errorret_t ret = slotToJSON(&slot, &json, &len); assert_true(errorIsOk(ret)); assert_true(saveSlotHasSaved(&slot.cachedData)); free(json); } static void test_saveSlotHasSaved_nullAsserts(void **state) { expect_assert_failure(saveSlotHasSaved(NULL)); } // ============================================================ // saveSlotWriteJSON // ============================================================ static void test_saveSlotWriteJSON_nullAsserts(void **state) { saveslot_t slot; saveSlotInit(&slot); writeInit(); expect_assert_failure(saveSlotWriteJSON(NULL, doc, object)); expect_assert_failure(saveSlotWriteJSON(&slot, NULL, object)); expect_assert_failure(saveSlotWriteJSON(&slot, doc, NULL)); yyjson_mut_doc_free(doc); } // ============================================================ // saveSlotReadJSON // ============================================================ static void test_saveSlotReadJSON_nullAsserts(void **state) { saveslot_t slot; saveSlotInit(&slot); yyjson_doc *readDoc = yyjson_read("{}", 2, 0); yyjson_val *readObject = yyjson_doc_get_root(readDoc); expect_assert_failure(saveSlotReadJSON(NULL, readObject)); expect_assert_failure(saveSlotReadJSON(&slot, NULL)); yyjson_doc_free(readDoc); } static void test_saveSlotReadJSON_roundTrip(void **state) { saveslot_t written; saveSlotInit(&written); written.version = 7;// deliberately non-default, not part of the JSON schema stringCopy(written.cachedData.name, "Hero", sizeof(written.cachedData.name)); written.cachedData.playerLevel = 42; char_t *json; size_t len; errorret_t writeRet = slotToJSON(&written, &json, &len); assert_true(errorIsOk(writeRet)); saveslot_t read; saveSlotInit(&read); errorret_t readRet = slotFromJSON(&read, json); assert_true(errorIsOk(readRet)); assert_true(stringEquals(read.cachedData.name, "Hero")); assert_int_equal(read.cachedData.playerLevel, 42); assert_true(read.cachedData.time.time == written.cachedData.time.time); // version/dataType are struct-only bookkeeping, never serialized - the // reader keeps whatever saveSlotInit gave it regardless of the writer's // version. assert_int_equal(read.version, 1); free(json); } static void test_saveSlotReadJSON_missingFieldsUseDefaults(void **state) { saveslot_t slot; saveSlotInit(&slot); stringCopy(slot.cachedData.name, "sentinel", sizeof(slot.cachedData.name)); slot.cachedData.playerLevel = 999; errorret_t ret = slotFromJSON( &slot, "{\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}" ); assert_true(errorIsOk(ret)); assert_int_equal(slot.cachedData.name[0], '\0'); assert_int_equal(slot.cachedData.playerLevel, 1); } static void test_saveSlotReadJSON_missingTimeKey_errors(void **state) { saveslot_t slot; saveSlotInit(&slot); errorret_t ret = slotFromJSON(&slot, "{\"name\":\"Hero\",\"playerLevel\":5}"); assert_true(errorIsNotOk(ret)); errorCatch(ret); } static void test_saveSlotReadJSON_nameTooLong_errors(void **state) { saveslot_t slot; saveSlotInit(&slot); errorret_t ret = slotFromJSON( &slot, "{\"name\":\"WayTooLongAName\"," "\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}" ); assert_true(errorIsNotOk(ret)); errorCatch(ret); } static void test_saveSlotReadJSON_nonObjectRoot_errors(void **state) { saveslot_t slot; saveSlotInit(&slot); // A non-object root has no keys, so every field falls back to its // default - except "time", which is not optional and errors instead. errorret_t ret = slotFromJSON(&slot, "[]"); assert_true(errorIsNotOk(ret)); errorCatch(ret); } int main(void) { assertInit(); const struct CMUnitTest tests[] = { cmocka_unit_test(test_saveSlotInit_defaults), cmocka_unit_test(test_saveSlotInit_nullAsserts), cmocka_unit_test(test_saveSlotInUse), cmocka_unit_test(test_saveSlotInUse_nullAsserts), cmocka_unit_test(test_saveSlotHasSaved), cmocka_unit_test(test_saveSlotHasSaved_nullAsserts), cmocka_unit_test(test_saveSlotWriteJSON_nullAsserts), cmocka_unit_test(test_saveSlotReadJSON_nullAsserts), cmocka_unit_test(test_saveSlotReadJSON_roundTrip), cmocka_unit_test(test_saveSlotReadJSON_missingFieldsUseDefaults), cmocka_unit_test(test_saveSlotReadJSON_missingTimeKey_errors), cmocka_unit_test(test_saveSlotReadJSON_nameTooLong_errors), cmocka_unit_test(test_saveSlotReadJSON_nonObjectRoot_errors), }; return cmocka_run_group_tests(tests, NULL, NULL); }