/** * 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/savedevicelinux.h" #include "save/saveslot.h" #include "save/savesettings.h" #include "util/memory.h" #include "util/string.h" #include #include // ============================================================ // Helper: drives a device through saveDeviceCheckAvailability + // saveDeviceUpdate (the real public flow), returning once the callback has // fired. // ============================================================ static bool_t g_callbackFired; static savedevice_t *g_callbackDevice; static void checkAvailabilityCallback(savedevice_t *device, void *user) { g_callbackFired = true; g_callbackDevice = device; } static void checkAvailabilityAndDrain(savedevice_t *device) { g_callbackFired = false; g_callbackDevice = NULL; saveDeviceCheckAvailability(device, checkAvailabilityCallback, NULL); saveDeviceUpdate(device); } // ============================================================ // saveDeviceLinuxGetDirectory / GetSettingsPath / GetSlotPath // ============================================================ static void test_getDirectory_success(void **state) { char_t path[512]; errorret_t ret = saveDeviceLinuxGetDirectory(path, sizeof(path)); assert_true(errorIsOk(ret)); char_t expected[512]; stringFormat(expected, sizeof(expected), "%s/.dusk/saves", getenv("HOME")); assert_true(stringEquals(path, expected)); } static void test_getDirectory_homeUnset_errors(void **state) { unsetenv("HOME"); char_t path[512]; errorret_t ret = saveDeviceLinuxGetDirectory(path, sizeof(path)); assert_true(errorIsNotOk(ret)); errorCatch(ret); } static void test_getDirectory_nullAsserts(void **state) { expect_assert_failure(saveDeviceLinuxGetDirectory(NULL, 512)); } static void test_getSettingsPath_success(void **state) { char_t path[512]; errorret_t ret = saveDeviceLinuxGetSettingsPath(path, sizeof(path)); assert_true(errorIsOk(ret)); char_t expected[512]; stringFormat( expected, sizeof(expected), "%s/.dusk/saves/settings.json", getenv("HOME") ); assert_true(stringEquals(path, expected)); } static void test_getSettingsPath_homeUnset_errors(void **state) { unsetenv("HOME"); char_t path[512]; errorret_t ret = saveDeviceLinuxGetSettingsPath(path, sizeof(path)); assert_true(errorIsNotOk(ret)); errorCatch(ret); } static void test_getSlotPath_success(void **state) { char_t path[512]; errorret_t ret = saveDeviceLinuxGetSlotPath(path, sizeof(path), 2); assert_true(errorIsOk(ret)); char_t expected[512]; stringFormat( expected, sizeof(expected), "%s/.dusk/saves/slot2.json", getenv("HOME") ); assert_true(stringEquals(path, expected)); } static void test_getSlotPath_homeUnset_errors(void **state) { unsetenv("HOME"); char_t path[512]; errorret_t ret = saveDeviceLinuxGetSlotPath(path, sizeof(path), 0); assert_true(errorIsNotOk(ret)); errorCatch(ret); } // ============================================================ // saveDeviceLinuxCheckAvailability // ============================================================ static void test_checkAvailability_wrongStateAsserts(void **state) { savedevice_t device; memoryZero(&device, sizeof(device)); device.state = SAVE_DEVICE_STATE_UNKNOWN; expect_assert_failure(saveDeviceLinuxCheckAvailability(&device)); } static void test_checkAvailability_nullAsserts(void **state) { expect_assert_failure(saveDeviceLinuxCheckAvailability(NULL)); } static void test_checkAvailability_success(void **state) { savedevice_t device; memoryZero(&device, sizeof(device)); device.state = SAVE_DEVICE_STATE_UNKNOWN; checkAvailabilityAndDrain(&device); assert_int_equal(device.state, SAVE_DEVICE_STATE_AVAILABLE); assert_null(device.reasonKey); assert_true(g_callbackFired); assert_ptr_equal(g_callbackDevice, &device); // The directory should now actually exist on disk (under the sandboxed // $HOME the fixture set up). char_t path[512]; errorret_t pathRet = saveDeviceLinuxGetDirectory(path, sizeof(path)); assert_true(errorIsOk(pathRet)); FILE *dir = fopen(path, "r"); // Directories can't be fopen'd for reading data, but a NULL here would // mean the path doesn't exist at all - a non-NULL/EISDIR failure both // indicate the directory exists. if(dir != NULL) fclose(dir); } static void test_checkAvailability_homeUnset(void **state) { unsetenv("HOME"); savedevice_t device; memoryZero(&device, sizeof(device)); device.state = SAVE_DEVICE_STATE_UNKNOWN; checkAvailabilityAndDrain(&device); assert_int_equal(device.state, SAVE_DEVICE_STATE_UNAVAILABLE); assert_true(stringEquals(device.reasonKey, "save.linux.no_home")); assert_true(g_callbackFired); } static void test_checkAvailability_mkdirpFails(void **state) { saveTestFixtureBlockSaveDirectory(); savedevice_t device; memoryZero(&device, sizeof(device)); device.state = SAVE_DEVICE_STATE_UNKNOWN; checkAvailabilityAndDrain(&device); assert_int_equal(device.state, SAVE_DEVICE_STATE_UNAVAILABLE); assert_true(stringEquals(device.reasonKey, "save.linux.mkdirp_failed")); assert_true(g_callbackFired); } // ============================================================ // saveDeviceLinuxSlotWrite / saveDeviceLinuxSlotRead // ============================================================ static void test_slotWrite_nullAsserts(void **state) { savedevice_t device; saveslot_t slot; saveSlotInit(&slot); expect_assert_failure(saveDeviceLinuxSlotWrite(NULL, &slot, 0)); expect_assert_failure(saveDeviceLinuxSlotWrite(&device, NULL, 0)); } static void test_slotRead_nullAsserts(void **state) { savedevice_t device; saveslot_t slot; saveSlotInit(&slot); expect_assert_failure(saveDeviceLinuxSlotRead(NULL, &slot, 0)); expect_assert_failure(saveDeviceLinuxSlotRead(&device, NULL, 0)); } static void test_slotWriteRead_roundTrip(void **state) { savedevice_t device; memoryZero(&device, sizeof(device)); // The save directory must exist before a write can succeed - normally // saveDeviceLinuxCheckAvailability does this via mkdirp. checkAvailabilityAndDrain(&device); saveslot_t written; saveSlotInit(&written); stringCopy(written.cachedData.name, "Hero", sizeof(written.cachedData.name)); written.cachedData.playerLevel = 9; errorret_t writeRet = saveDeviceLinuxSlotWrite(&device, &written, 1); assert_true(errorIsOk(writeRet)); saveslot_t read; saveSlotInit(&read); errorret_t readRet = saveDeviceLinuxSlotRead(&device, &read, 1); assert_true(errorIsOk(readRet)); assert_true(stringEquals(read.cachedData.name, "Hero")); assert_int_equal(read.cachedData.playerLevel, 9); } static void test_slotRead_noFileYet_leavesSlotUntouched(void **state) { savedevice_t device; memoryZero(&device, sizeof(device)); saveslot_t slot; saveSlotInit(&slot); stringCopy(slot.cachedData.name, "sentinel", sizeof(slot.cachedData.name)); slot.cachedData.playerLevel = 555; errorret_t ret = saveDeviceLinuxSlotRead(&device, &slot, 2); assert_true(errorIsOk(ret)); assert_true(stringEquals(slot.cachedData.name, "sentinel")); assert_int_equal(slot.cachedData.playerLevel, 555); } static void test_slotRead_corruptFile_errors(void **state) { savedevice_t device; memoryZero(&device, sizeof(device)); // The directory must exist before we can drop a file into it - normally // saveDeviceLinuxCheckAvailability does this via mkdirp. savedevice_t tmpDevice; memoryZero(&tmpDevice, sizeof(tmpDevice)); tmpDevice.state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY; saveDeviceLinuxCheckAvailability(&tmpDevice); assert_int_equal(tmpDevice.state, SAVE_DEVICE_STATE_AVAILABLE); char_t slotPath[512]; errorret_t pathRet = saveDeviceLinuxGetSlotPath(slotPath, sizeof(slotPath), 0); assert_true(errorIsOk(pathRet)); FILE *file = fopen(slotPath, "w"); assert_non_null(file); fprintf(file, "not valid json {{{"); fclose(file); saveslot_t slot; saveSlotInit(&slot); errorret_t ret = saveDeviceLinuxSlotRead(&device, &slot, 0); assert_true(errorIsNotOk(ret)); errorCatch(ret); } // ============================================================ // saveDeviceLinuxSettingsWrite / saveDeviceLinuxSettingsRead // ============================================================ static void test_settingsWrite_nullAsserts(void **state) { savedevice_t device; savesettings_t settings; saveSettingsInit(&settings); expect_assert_failure(saveDeviceLinuxSettingsWrite(NULL, &settings)); expect_assert_failure(saveDeviceLinuxSettingsWrite(&device, NULL)); } static void test_settingsRead_nullAsserts(void **state) { savedevice_t device; savesettings_t settings; saveSettingsInit(&settings); expect_assert_failure(saveDeviceLinuxSettingsRead(NULL, &settings)); expect_assert_failure(saveDeviceLinuxSettingsRead(&device, NULL)); } static void test_settingsWriteRead_roundTrip(void **state) { savedevice_t device; memoryZero(&device, sizeof(device)); // The save directory must exist before a write can succeed - normally // saveDeviceLinuxCheckAvailability does this via mkdirp. checkAvailabilityAndDrain(&device); savesettings_t written; saveSettingsInit(&written); written.someSetting = 314; errorret_t writeRet = saveDeviceLinuxSettingsWrite(&device, &written); assert_true(errorIsOk(writeRet)); savesettings_t read; saveSettingsInit(&read); errorret_t readRet = saveDeviceLinuxSettingsRead(&device, &read); assert_true(errorIsOk(readRet)); assert_int_equal(read.someSetting, 314); } static void test_settingsRead_noFileYet_leavesUntouched(void **state) { savedevice_t device; memoryZero(&device, sizeof(device)); savesettings_t settings; saveSettingsInit(&settings); settings.someSetting = 777; errorret_t ret = saveDeviceLinuxSettingsRead(&device, &settings); assert_true(errorIsOk(ret)); assert_int_equal(settings.someSetting, 777); } static void test_settingsRead_corruptFile_errors(void **state) { savedevice_t device; memoryZero(&device, sizeof(device)); savedevice_t tmpDevice; memoryZero(&tmpDevice, sizeof(tmpDevice)); tmpDevice.state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY; saveDeviceLinuxCheckAvailability(&tmpDevice); assert_int_equal(tmpDevice.state, SAVE_DEVICE_STATE_AVAILABLE); char_t settingsPath[512]; errorret_t pathRet = saveDeviceLinuxGetSettingsPath(settingsPath, sizeof(settingsPath)); assert_true(errorIsOk(pathRet)); FILE *file = fopen(settingsPath, "w"); assert_non_null(file); fprintf(file, "not valid json {{{"); fclose(file); savesettings_t settings; saveSettingsInit(&settings); errorret_t ret = saveDeviceLinuxSettingsRead(&device, &settings); assert_true(errorIsNotOk(ret)); errorCatch(ret); } int main(void) { assertInit(); const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown( test_getDirectory_success, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_getDirectory_homeUnset_errors, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_getDirectory_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_getSettingsPath_success, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_getSettingsPath_homeUnset_errors, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_getSlotPath_success, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_getSlotPath_homeUnset_errors, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_checkAvailability_wrongStateAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_checkAvailability_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_checkAvailability_success, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_checkAvailability_homeUnset, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_checkAvailability_mkdirpFails, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_slotWrite_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_slotRead_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_slotWriteRead_roundTrip, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_slotRead_noFileYet_leavesSlotUntouched, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_slotRead_corruptFile_errors, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_settingsWrite_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_settingsRead_nullAsserts, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_settingsWriteRead_roundTrip, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_settingsRead_noFileYet_leavesUntouched, saveTestFixtureSetup, saveTestFixtureTeardown ), cmocka_unit_test_setup_teardown( test_settingsRead_corruptFile_errors, saveTestFixtureSetup, saveTestFixtureTeardown ), }; return cmocka_run_group_tests(tests, NULL, NULL); }