party.c's functions no longer take a party_t* - they always operate on SAVE.slot.party, since gameplay only ever has one active party. That forced the same change on saveSlotInit/saveSlotNewGame (they called partyInit/partyAddMember internally), so those are now parameterless too, always resetting/seeding SAVE.slot. Callers building a save at a specific index (new-game creation, slot deletion) now set SAVE.slotCurrent first instead of building an independent local struct - safe since the select-save screen is only ever reached before any gameplay starts. saveSlotWriteJSON/saveSlotReadJSON deliberately keep their explicit saveslot_t* parameter - they're genuine serialization primitives used by the save-device layer and tests against arbitrary structs, unrelated to the single-current-party assumption. saveSlotReadJSON's internal reset is now a plain memset instead of delegating to the now-global-only saveSlotInit(). Updates test_save/test_savedevice/test_savedevicelinux/test_saveslot for the new signatures. Note: test_savedevice/test_savedevicelinux/test_save have pre-existing failures in this sandbox unrelated to this change - savetestfixture.c swaps $HOME, but saveDeviceLinuxGetDirectory actually derives the save path from ASSET.baseDirectory (the executable's own location), so the fixture's sandboxing never actually applies. test_ saveslot.c (which doesn't depend on that fixture) passes 15/15. Co-Authored-By: Claude Sonnet 5 <[email protected]>
343 lines
11 KiB
C
343 lines
11 KiB
C
/**
|
|
* 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/slot/saveslot.h"
|
|
#include "save/settings/savesettings.h"
|
|
#include "save/save.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;
|
|
// Content is never touched - both calls assert on a NULL argument
|
|
// before either device or slot would actually be read.
|
|
saveslot_t 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;
|
|
|
|
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);
|
|
|
|
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
|
|
// write source here.
|
|
saveSlotInit();
|
|
SAVE.slot.cachedData.playerLevel = 21;
|
|
|
|
errorret_t writeRet = saveDeviceSlotWrite(&device, &SAVE.slot, 0);
|
|
assert_true(errorIsOk(writeRet));
|
|
|
|
// No pre-init needed - saveSlotReadJSON() (via saveDeviceSlotRead) fully
|
|
// resets/overwrites its destination struct regardless of prior content.
|
|
saveslot_t 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.audioMasterVolume = 0.25f;// exactly representable, safe to compare
|
|
|
|
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_true(read.audioMasterVolume == 0.25f);
|
|
}
|
|
|
|
// ============================================================
|
|
// 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);
|
|
}
|