/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "dusktest.h" #include "savetestfixture.h" #include "save/savedevice.h" #include "save/saveslot.h" #include "save/savesettings.h" #include "util/memory.h" #include "util/string.h" // ============================================================ // saveDeviceInit // ============================================================ static void test_saveDeviceInit_nullAsserts(void **state) { expect_assert_failure(saveDeviceInit(NULL)); } static void test_saveDeviceInit_setsUnknownState(void **state) { savedevice_t device; memorySet(&device, 0xAB, sizeof(device)); errorret_t ret = saveDeviceInit(&device); assert_true(errorIsOk(ret)); assert_int_equal(device.state, SAVE_DEVICE_STATE_UNKNOWN); assert_false(device.fireCallback); } // ============================================================ // saveDeviceUpdate / saveDeviceFireCallback // ============================================================ static void test_saveDeviceUpdate_nullAsserts(void **state) { expect_assert_failure(saveDeviceUpdate(NULL)); } static bool_t g_updateCallbackFired; static void *g_updateCallbackUser; static void updateCallback(savedevice_t *device, void *user) { g_updateCallbackFired = true; g_updateCallbackUser = user; } static void test_saveDeviceUpdate_firesQueuedCallbackOnce(void **state) { savedevice_t device; errorret_t initRet = saveDeviceInit(&device); assert_true(errorIsOk(initRet)); device.stateCallback = updateCallback; device.user = (void *)0x1234; device.fireCallback = true; g_updateCallbackFired = false; g_updateCallbackUser = NULL; errorret_t ret = saveDeviceUpdate(&device); assert_true(errorIsOk(ret)); assert_true(g_updateCallbackFired); assert_ptr_equal(g_updateCallbackUser, (void *)0x1234); assert_false(device.fireCallback); // A second update with nothing queued must not re-fire the callback. g_updateCallbackFired = false; ret = saveDeviceUpdate(&device); assert_true(errorIsOk(ret)); assert_false(g_updateCallbackFired); } static void test_saveDeviceFireCallback_nullAsserts(void **state) { expect_assert_failure(saveDeviceFireCallback(NULL)); } static void test_saveDeviceFireCallback_setsFlag(void **state) { savedevice_t device; errorret_t initRet = saveDeviceInit(&device); assert_true(errorIsOk(initRet)); saveDeviceFireCallback(&device); assert_true(device.fireCallback); } static void test_saveDeviceFireCallback_assertsIfAlreadyQueued(void **state) { savedevice_t device; errorret_t initRet = saveDeviceInit(&device); assert_true(errorIsOk(initRet)); saveDeviceFireCallback(&device); expect_assert_failure(saveDeviceFireCallback(&device)); } // ============================================================ // saveDeviceCheckAvailability // ============================================================ static void test_saveDeviceCheckAvailability_nullAsserts(void **state) { savedevice_t device; errorret_t initRet = saveDeviceInit(&device); assert_true(errorIsOk(initRet)); expect_assert_failure( saveDeviceCheckAvailability(NULL, updateCallback, NULL) ); expect_assert_failure(saveDeviceCheckAvailability(&device, NULL, NULL)); } static void test_saveDeviceCheckAvailability_assertsWhileAlreadyChecking( void **state ) { savedevice_t device; errorret_t initRet = saveDeviceInit(&device); assert_true(errorIsOk(initRet)); // Directly force the device into a mid-check state without going through // the real (synchronous, on this platform) availability check - this // simulates what a genuinely async platform would look like mid-flight. device.state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY; expect_assert_failure( saveDeviceCheckAvailability(&device, updateCallback, NULL) ); } static void test_saveDeviceCheckAvailability_resolvesSynchronouslyOnLinux( void **state ) { savedevice_t device; errorret_t initRet = saveDeviceInit(&device); assert_true(errorIsOk(initRet)); g_updateCallbackFired = false; saveDeviceCheckAvailability(&device, updateCallback, NULL); // The Linux backend resolves availability synchronously inside the check // call itself, but the callback is only *queued* (fireCallback), not // fired, until an explicit saveDeviceUpdate() drains it. assert_int_equal(device.state, SAVE_DEVICE_STATE_AVAILABLE); assert_true(device.fireCallback); assert_false(g_updateCallbackFired); errorret_t updateRet = saveDeviceUpdate(&device); assert_true(errorIsOk(updateRet)); assert_true(g_updateCallbackFired); } // ============================================================ // saveDeviceSlotWrite / saveDeviceSlotRead / saveDeviceSettingsWrite / // saveDeviceSettingsRead - dispatch to the platform's four hooks. // ============================================================ static void test_saveDeviceSlotWrite_nullAsserts(void **state) { savedevice_t device; saveslot_t slot; saveSlotInit(&slot); expect_assert_failure(saveDeviceSlotWrite(NULL, &slot, 0)); expect_assert_failure(saveDeviceSlotWrite(&device, NULL, 0)); } static void test_saveDeviceSlotRead_nullAsserts(void **state) { savedevice_t device; saveslot_t slot; saveSlotInit(&slot); expect_assert_failure(saveDeviceSlotRead(NULL, &slot, 0)); expect_assert_failure(saveDeviceSlotRead(&device, NULL, 0)); } static void test_saveDeviceSettingsWrite_nullAsserts(void **state) { savedevice_t device; savesettings_t settings; saveSettingsInit(&settings); expect_assert_failure(saveDeviceSettingsWrite(NULL, &settings)); expect_assert_failure(saveDeviceSettingsWrite(&device, NULL)); } static void test_saveDeviceSettingsRead_nullAsserts(void **state) { savedevice_t device; savesettings_t settings; saveSettingsInit(&settings); expect_assert_failure(saveDeviceSettingsRead(NULL, &settings)); expect_assert_failure(saveDeviceSettingsRead(&device, NULL)); } static void test_saveDeviceSlot_dispatchesToPlatform(void **state) { savedevice_t device; errorret_t initRet = saveDeviceInit(&device); assert_true(errorIsOk(initRet)); // The save directory must exist before a write can succeed - normally // saveDeviceCheckAvailability does this via the platform's mkdirp. saveDeviceCheckAvailability(&device, updateCallback, NULL); saveDeviceUpdate(&device); saveslot_t written; saveSlotInit(&written); written.cachedData.playerLevel = 21; errorret_t writeRet = saveDeviceSlotWrite(&device, &written, 0); assert_true(errorIsOk(writeRet)); saveslot_t read; saveSlotInit(&read); errorret_t readRet = saveDeviceSlotRead(&device, &read, 0); assert_true(errorIsOk(readRet)); assert_int_equal(read.cachedData.playerLevel, 21); } static void test_saveDeviceSettings_dispatchesToPlatform(void **state) { savedevice_t device; errorret_t initRet = saveDeviceInit(&device); assert_true(errorIsOk(initRet)); // The save directory must exist before a write can succeed - normally // saveDeviceCheckAvailability does this via the platform's mkdirp. saveDeviceCheckAvailability(&device, updateCallback, NULL); saveDeviceUpdate(&device); savesettings_t written; saveSettingsInit(&written); written.someSetting = 88; errorret_t writeRet = saveDeviceSettingsWrite(&device, &written); assert_true(errorIsOk(writeRet)); savesettings_t read; saveSettingsInit(&read); errorret_t readRet = saveDeviceSettingsRead(&device, &read); assert_true(errorIsOk(readRet)); assert_int_equal(read.someSetting, 88); } // ============================================================ // saveDeviceDispose // ============================================================ static void test_saveDeviceDispose_nullAsserts(void **state) { expect_assert_failure(saveDeviceDispose(NULL)); } static void test_saveDeviceDispose_succeeds(void **state) { savedevice_t device; errorret_t initRet = saveDeviceInit(&device); assert_true(errorIsOk(initRet)); errorret_t ret = saveDeviceDispose(&device); assert_true(errorIsOk(ret)); } int main(void) { assertInit(); const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown( test_saveDeviceInit_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceInit_setsUnknownState, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceUpdate_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceUpdate_firesQueuedCallbackOnce, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceFireCallback_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceFireCallback_setsFlag, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceFireCallback_assertsIfAlreadyQueued, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceCheckAvailability_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceCheckAvailability_assertsWhileAlreadyChecking, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceCheckAvailability_resolvesSynchronouslyOnLinux, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceSlotWrite_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceSlotRead_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceSettingsWrite_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceSettingsRead_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceSlot_dispatchesToPlatform, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceSettings_dispatchesToPlatform, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceDispose_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_saveDeviceDispose_succeeds, saveTestFixtureSetup, saveTestFixtureTeardown ), }; return cmocka_run_group_tests(tests, NULL, NULL); }