Files
dusk/test/save/test_savedevice.c
T
YourWishesandClaude Sonnet 5 7a9a2b9832 Add order-based party roster, back-navigation in battle selection, fix win-state cutscene leak
- Battle selection menu can now go back a fighter to re-decide their move,
  and battle win/loss no longer leaks the round's remaining queued move
  cutscenes into the ending state (cutsceneSystemDiscardQueued).
- Party roster is now driven by party.jsonc: every defined member always
  exists (partyInit), while party->order alone determines who's currently
  active - partyAddMember/partyRemoveMember (and their cutscene items) now
  activate/deactivate an existing roster member by index instead of
  creating/destroying one, with removal compacting order to stay packed.
- New PARTY_ADD_MEMBER/PARTY_REMOVE_MEMBER/PARTY_HEAL_MEMBER/PARTY_HEAL_ALL
  cutscene items.
- saveSlotInit/partyInit now return errorret_t and take their target
  struct directly (saveslot_t*/party_t*) instead of implicitly operating
  on SAVE.slot; saveSlotReadJSON resets through saveSlotInit rather than
  duplicating the reset logic.
- Test coverage: stub the JSON asset loader in save test fixtures so
  partyInit's real party.jsonc load runs against an in-memory doc instead
  of hanging with no asset system initialized.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-21 22:27:46 -05:00

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);
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);
}