Files
dusk/test/save/test_savedevicelinux.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

456 lines
14 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/savedevicelinux.h"
#include "save/slot/saveslot.h"
#include "save/settings/savesettings.h"
#include "save/save.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;
// 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(saveDeviceLinuxSlotWrite(NULL, &slot, 0));
expect_assert_failure(saveDeviceLinuxSlotWrite(&device, NULL, 0));
}
static void test_slotRead_nullAsserts(void **state) {
savedevice_t device;
saveslot_t 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);
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
// write source here.
saveSlotInit(&SAVE.slot);
stringCopy(
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
);
SAVE.slot.cachedData.playerLevel = 9;
errorret_t writeRet = saveDeviceLinuxSlotWrite(&device, &SAVE.slot, 1);
assert_true(errorIsOk(writeRet));
// No pre-init needed - saveSlotReadJSON() (via saveDeviceLinuxSlotRead)
// fully resets/overwrites its destination struct regardless of prior
// content.
saveslot_t 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));
// Deliberately not saveSlotInit()'d - a missing slot file means
// saveDeviceLinuxSlotRead never even calls saveSlotReadJSON, so slot
// must come in exactly as the caller left it and go out unchanged.
saveslot_t slot;
memoryZero(&slot, sizeof(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;
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.audioMasterVolume = 3.25f;// exactly representable, safe to compare
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_true(read.audioMasterVolume == 3.25f);
}
static void test_settingsRead_noFileYet_leavesUntouched(void **state) {
savedevice_t device;
memoryZero(&device, sizeof(device));
savesettings_t settings;
saveSettingsInit(&settings);
settings.audioMasterVolume = 7.75f;
errorret_t ret = saveDeviceLinuxSettingsRead(&device, &settings);
assert_true(errorIsOk(ret));
assert_true(settings.audioMasterVolume == 7.75f);
}
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);
}