Add a real test suite for the save system (previously had none)
Covers save.c (device discovery/orchestration), savedevice.c (the generic device state machine and platform dispatch), and the Linux platform backend (path building, availability checks, JSON read/write, corrupt/missing-file handling), plus saveslot.c/savesettings.c JSON round-trips. 88 tests across 5 files, all run against the real Linux filesystem backend sandboxed to a temp $HOME (there's no mockable platform layer - the hooks are compile-time macros, not function pointers). Deliberately locks in two existing behaviors rather than working around them: saveSaveSettings() is a permanent no-op because nothing anywhere ever sets SAVE.settingsDirty = true, and saveUpdate() unconditionally rewrites settings back out the moment a device is found regardless of that same dirty flag. Both are pre-existing, not introduced here. Does not cover the SAVE_DEVICE_DATA_RAW blob codec (PSP/GameCube/Wii only) or GameCube's 2-device fallback chain - neither compiles into the Linux host test build. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* 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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// ============================================================
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user