rpg/ui/scene moved to archive/ earlier left a handful of otherwise-generic systems with dead references into them: - asset/loader/chunk existed solely to load rpg overworld map chunks (tile_t, worldpos_t) - deleted outright, along with its ASSET_LOADER_TYPE_CHUNK entry in assetloader.c/h. - save/slot wrapped party_t/storyflagval_t/map-name/cutscene-path, which is 100% rpg save data - deleted outright (save.c/h, savedevice.c/h and the Linux save backend keep their generic settings/device-discovery logic, just lose the slot read/write pieces that only existed for it). - locale/localemanager.c and test/CMakeLists.txt/test/save/* had a few stray includes and test cases that only existed for the now-gone code. Also restores assets/locale/en_US.po (copied back from archive/assets/) - locale init still needs a real locale file to boot into, even with the asset-moving/archiving done separately. Co-Authored-By: Claude Sonnet 5 <[email protected]>
400 lines
13 KiB
C
400 lines
13 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/save.h"
|
|
#include "util/memory.h"
|
|
#include "util/string.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
// ============================================================
|
|
// Helpers
|
|
// ============================================================
|
|
|
|
static void noopAvailabilityCallback(savedevice_t *device, void *user) {}
|
|
|
|
// Skips saveFindAvailableDevice()'s bookkeeping (findingAvailableDevice
|
|
// etc.) and goes straight to "device 0 is current" - used by tests that
|
|
// only care about behavior once a device has been selected, not how it got
|
|
// selected. Still runs the real availability check underneath, since that
|
|
// is what actually creates the on-disk save directory via mkdirp - reads
|
|
// and writes fail without it.
|
|
static void makeDeviceAvailable(void) {
|
|
saveDeviceCheckAvailability(&SAVE.devices[0], noopAvailabilityCallback, NULL);
|
|
saveDeviceUpdate(&SAVE.devices[0]);
|
|
assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_AVAILABLE);
|
|
SAVE.deviceCurrent = 0;
|
|
}
|
|
|
|
static bool_t g_findCallbackFired;
|
|
static savedevice_t *g_findCallbackDevice;
|
|
|
|
static void findAvailableCallback(savedevice_t *device, void *user) {
|
|
g_findCallbackFired = true;
|
|
g_findCallbackDevice = device;
|
|
}
|
|
|
|
static void resetFindCallbackState(void) {
|
|
g_findCallbackFired = false;
|
|
g_findCallbackDevice = NULL;
|
|
}
|
|
|
|
// ============================================================
|
|
// saveInit
|
|
// ============================================================
|
|
|
|
static void test_saveInit_setsDefaults(void **state) {
|
|
// saveTestFixtureSetup already called the real saveInit() - just assert
|
|
// on the state it left behind.
|
|
assert_int_equal(SAVE.deviceCurrent, 0xFF);
|
|
assert_false(SAVE.findingAvailableDevice);
|
|
assert_false(SAVE.noAvailableDeviceFound);
|
|
assert_true(SAVE.settings.audioMasterVolume == 1.0f);
|
|
assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_UNKNOWN);
|
|
}
|
|
|
|
// ============================================================
|
|
// saveUpdate - no-op path
|
|
// ============================================================
|
|
|
|
static void test_saveUpdate_noopWhenNotFinding(void **state) {
|
|
errorret_t ret = saveUpdate();
|
|
assert_true(errorIsOk(ret));
|
|
assert_int_equal(SAVE.deviceCurrent, 0xFF);
|
|
assert_false(SAVE.findingAvailableDevice);
|
|
}
|
|
|
|
// ============================================================
|
|
// saveFindAvailableDevice / saveUpdate - discovery flow
|
|
// ============================================================
|
|
|
|
static void test_saveFindAvailableDevice_nullCallbackAsserts(void **state) {
|
|
expect_assert_failure(saveFindAvailableDevice(NULL, NULL));
|
|
}
|
|
|
|
static void test_saveFindAvailableDevice_reentrantAsserts(void **state) {
|
|
resetFindCallbackState();
|
|
saveFindAvailableDevice(findAvailableCallback, NULL);
|
|
expect_assert_failure(saveFindAvailableDevice(findAvailableCallback, NULL));
|
|
}
|
|
|
|
static void test_saveFindAvailableDevice_deviceAlreadyAvailable(void **state) {
|
|
resetFindCallbackState();
|
|
// Mark the device available ahead of time via a real check (this is also
|
|
// what creates the on-disk save directory that the "found device" flow
|
|
// below needs to write settings back out to).
|
|
saveDeviceCheckAvailability(&SAVE.devices[0], noopAvailabilityCallback, NULL);
|
|
saveDeviceUpdate(&SAVE.devices[0]);
|
|
assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_AVAILABLE);
|
|
|
|
saveFindAvailableDevice(findAvailableCallback, NULL);
|
|
|
|
// Set synchronously inside the call itself - no need to wait for update.
|
|
assert_int_equal(SAVE.deviceCurrent, 0);
|
|
assert_false(g_findCallbackFired);
|
|
|
|
errorret_t ret = saveUpdate();
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(g_findCallbackFired);
|
|
assert_ptr_equal(g_findCallbackDevice, &SAVE.devices[0]);
|
|
assert_false(SAVE.findingAvailableDevice);
|
|
}
|
|
|
|
static void test_saveFindAvailableDevice_coldPath_oneUpdateResolves(
|
|
void **state
|
|
) {
|
|
resetFindCallbackState();
|
|
|
|
saveFindAvailableDevice(findAvailableCallback, NULL);
|
|
|
|
// Nothing resolved synchronously yet - the Linux availability check is
|
|
// synchronous, but the callback it queues is only drained by an explicit
|
|
// saveDeviceUpdate() inside saveUpdate().
|
|
assert_int_equal(SAVE.deviceCurrent, 0xFF);
|
|
assert_false(g_findCallbackFired);
|
|
|
|
errorret_t ret = saveUpdate();
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(g_findCallbackFired);
|
|
assert_ptr_equal(g_findCallbackDevice, &SAVE.devices[0]);
|
|
assert_int_equal(SAVE.deviceCurrent, 0);
|
|
assert_false(SAVE.findingAvailableDevice);
|
|
}
|
|
|
|
static void test_saveFindAvailableDevice_allUnavailable_firesNullCallback(
|
|
void **state
|
|
) {
|
|
unsetenv("HOME");
|
|
resetFindCallbackState();
|
|
|
|
saveFindAvailableDevice(findAvailableCallback, NULL);
|
|
|
|
errorret_t ret = saveUpdate();
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(g_findCallbackFired);
|
|
assert_null(g_findCallbackDevice);
|
|
assert_int_equal(SAVE.deviceCurrent, 0xFF);
|
|
assert_false(SAVE.findingAvailableDevice);
|
|
}
|
|
|
|
// ============================================================
|
|
// saveOnDeviceAvailabilityChecked
|
|
// ============================================================
|
|
|
|
static void test_saveOnDeviceAvailabilityChecked_nullDeviceAsserts(
|
|
void **state
|
|
) {
|
|
SAVE.findingAvailableDevice = true;
|
|
expect_assert_failure(saveOnDeviceAvailabilityChecked(NULL, NULL));
|
|
}
|
|
|
|
static void test_saveOnDeviceAvailabilityChecked_notFindingAsserts(
|
|
void **state
|
|
) {
|
|
SAVE.findingAvailableDevice = false;
|
|
expect_assert_failure(
|
|
saveOnDeviceAvailabilityChecked(&SAVE.devices[0], NULL)
|
|
);
|
|
}
|
|
|
|
static void test_saveOnDeviceAvailabilityChecked_ignoresIfAlreadyResolved(
|
|
void **state
|
|
) {
|
|
SAVE.findingAvailableDevice = true;
|
|
SAVE.deviceCurrent = 0;
|
|
SAVE.devices[0].state = SAVE_DEVICE_STATE_UNAVAILABLE;
|
|
|
|
saveOnDeviceAvailabilityChecked(&SAVE.devices[0], NULL);
|
|
|
|
// Already resolved - unavailable status on a re-delivered callback must
|
|
// not clobber the winning device or flip noAvailableDeviceFound.
|
|
assert_int_equal(SAVE.deviceCurrent, 0);
|
|
assert_false(SAVE.noAvailableDeviceFound);
|
|
}
|
|
|
|
static void test_saveOnDeviceAvailabilityChecked_singleDeviceUnavailable(
|
|
void **state
|
|
) {
|
|
SAVE.findingAvailableDevice = true;
|
|
SAVE.deviceCurrent = 0xFF;
|
|
SAVE.devices[0].state = SAVE_DEVICE_STATE_UNAVAILABLE;
|
|
|
|
saveOnDeviceAvailabilityChecked(&SAVE.devices[0], NULL);
|
|
|
|
// SAVE_DEVICE_COUNT is 1 on Linux, so an unavailable device 0 is always
|
|
// "the last device" - there's no next device to chain to.
|
|
assert_true(SAVE.noAvailableDeviceFound);
|
|
assert_int_equal(SAVE.deviceCurrent, 0xFF);
|
|
}
|
|
|
|
static void test_saveOnDeviceAvailabilityChecked_deviceAvailable(
|
|
void **state
|
|
) {
|
|
SAVE.findingAvailableDevice = true;
|
|
SAVE.deviceCurrent = 0xFF;
|
|
SAVE.devices[0].state = SAVE_DEVICE_STATE_AVAILABLE;
|
|
|
|
saveOnDeviceAvailabilityChecked(&SAVE.devices[0], NULL);
|
|
|
|
assert_int_equal(SAVE.deviceCurrent, 0);
|
|
assert_false(SAVE.noAvailableDeviceFound);
|
|
}
|
|
|
|
// ============================================================
|
|
// saveSaveSettings / saveLoadSettings
|
|
// ============================================================
|
|
|
|
static void test_saveSaveSettings_noCurrentDeviceAsserts(void **state) {
|
|
expect_assert_failure(saveSaveSettings());
|
|
}
|
|
|
|
static void test_saveSaveSettings_notDirty_isNoop(void **state) {
|
|
// Documents a real, currently-shipping bug: nothing anywhere in the
|
|
// codebase ever sets SAVE.settingsDirty = true, so saveSaveSettings() is
|
|
// permanently a no-op in practice. This test locks in that (surprising)
|
|
// behavior rather than the presumably-intended one, so it fails loudly
|
|
// if someone changes the no-op condition without meaning to.
|
|
makeDeviceAvailable();
|
|
assert_false(SAVE.settingsDirty);
|
|
SAVE.settings.audioMasterVolume = 12.0f;
|
|
|
|
errorret_t ret = saveSaveSettings();
|
|
assert_true(errorIsOk(ret));
|
|
|
|
savesettings_t onDisk;
|
|
saveSettingsInit(&onDisk);
|
|
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
|
assert_true(errorIsOk(readRet));
|
|
assert_true(onDisk.audioMasterVolume == 1.0f);// never actually written
|
|
}
|
|
|
|
static void test_saveSaveSettings_dirtyForced_writesAndClearsFlag(
|
|
void **state
|
|
) {
|
|
makeDeviceAvailable();
|
|
SAVE.settingsDirty = true;// nothing in real code sets this - forced here
|
|
SAVE.settings.audioMasterVolume = 5.5f;
|
|
|
|
errorret_t ret = saveSaveSettings();
|
|
assert_true(errorIsOk(ret));
|
|
assert_false(SAVE.settingsDirty);
|
|
|
|
savesettings_t onDisk;
|
|
saveSettingsInit(&onDisk);
|
|
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
|
assert_true(errorIsOk(readRet));
|
|
assert_true(onDisk.audioMasterVolume == 5.5f);
|
|
}
|
|
|
|
static void test_saveLoadSettings_noCurrentDeviceAsserts(void **state) {
|
|
expect_assert_failure(saveLoadSettings());
|
|
}
|
|
|
|
static void test_saveLoadSettings_readsAndClearsDirtyFlag(void **state) {
|
|
makeDeviceAvailable();
|
|
|
|
savesettings_t onDisk;
|
|
saveSettingsInit(&onDisk);
|
|
onDisk.audioMasterVolume = 9.25f;
|
|
errorret_t writeRet = saveDeviceSettingsWrite(&SAVE.devices[0], &onDisk);
|
|
assert_true(errorIsOk(writeRet));
|
|
|
|
SAVE.settingsDirty = true;
|
|
SAVE.settings.audioMasterVolume = 0.0f;
|
|
|
|
errorret_t ret = saveLoadSettings();
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(SAVE.settings.audioMasterVolume == 9.25f);
|
|
assert_false(SAVE.settingsDirty);
|
|
}
|
|
|
|
// ============================================================
|
|
// saveDispose
|
|
// ============================================================
|
|
|
|
static void test_saveDispose_succeeds(void **state) {
|
|
errorret_t ret = saveDispose();
|
|
assert_true(errorIsOk(ret));
|
|
}
|
|
|
|
// ============================================================
|
|
// saveUpdate - the unconditional settings write-back on device found
|
|
// ============================================================
|
|
|
|
static void test_saveUpdate_foundDevice_rewritesSettingsUnconditionally(
|
|
void **state
|
|
) {
|
|
// Documents another surprising bit of current behavior: as soon as a
|
|
// device is found, saveUpdate() writes SAVE.settings straight back out to
|
|
// it, regardless of settingsDirty - independent of (and unguarded by) the
|
|
// same dirty-flag check saveSaveSettings() itself honors.
|
|
resetFindCallbackState();
|
|
SAVE.settings.audioMasterVolume = 3.25f;
|
|
assert_false(SAVE.settingsDirty);
|
|
|
|
saveFindAvailableDevice(findAvailableCallback, NULL);
|
|
errorret_t ret = saveUpdate();
|
|
assert_true(errorIsOk(ret));
|
|
assert_true(g_findCallbackFired);
|
|
|
|
savesettings_t onDisk;
|
|
saveSettingsInit(&onDisk);
|
|
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
|
assert_true(errorIsOk(readRet));
|
|
assert_true(onDisk.audioMasterVolume == 3.25f);
|
|
}
|
|
|
|
int main(void) {
|
|
assertInit();
|
|
const struct CMUnitTest tests[] = {
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveInit_setsDefaults, saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveUpdate_noopWhenNotFinding,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveFindAvailableDevice_nullCallbackAsserts,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveFindAvailableDevice_reentrantAsserts,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveFindAvailableDevice_deviceAlreadyAvailable,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveFindAvailableDevice_coldPath_oneUpdateResolves,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveFindAvailableDevice_allUnavailable_firesNullCallback,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveOnDeviceAvailabilityChecked_nullDeviceAsserts,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveOnDeviceAvailabilityChecked_notFindingAsserts,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveOnDeviceAvailabilityChecked_ignoresIfAlreadyResolved,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveOnDeviceAvailabilityChecked_singleDeviceUnavailable,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveOnDeviceAvailabilityChecked_deviceAvailable,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveSaveSettings_noCurrentDeviceAsserts,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveSaveSettings_notDirty_isNoop,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveSaveSettings_dirtyForced_writesAndClearsFlag,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveLoadSettings_noCurrentDeviceAsserts,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveLoadSettings_readsAndClearsDirtyFlag,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveDispose_succeeds, saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
|
|
cmocka_unit_test_setup_teardown(
|
|
test_saveUpdate_foundDevice_rewritesSettingsUnconditionally,
|
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
|
),
|
|
};
|
|
|
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
|
}
|