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:
@@ -12,5 +12,6 @@ add_subdirectory(thread)
|
|||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
add_subdirectory(rpg)
|
add_subdirectory(rpg)
|
||||||
add_subdirectory(item)
|
add_subdirectory(item)
|
||||||
|
add_subdirectory(save)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
add_subdirectory(util)
|
add_subdirectory(util)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
include(dusktest)
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
dusktest(test_savesettings.c)
|
||||||
|
dusktest(test_saveslot.c)
|
||||||
|
|
||||||
|
dusktest(test_savedevicelinux.c)
|
||||||
|
target_sources(test_savedevicelinux PRIVATE savetestfixture.c)
|
||||||
|
|
||||||
|
dusktest(test_savedevice.c)
|
||||||
|
target_sources(test_savedevice PRIVATE savetestfixture.c)
|
||||||
|
|
||||||
|
dusktest(test_save.c)
|
||||||
|
target_sources(test_save PRIVATE savetestfixture.c)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
// nftw()/FTW_DEPTH/FTW_PHYS are glibc extensions gated behind this.
|
||||||
|
#define _XOPEN_SOURCE 700
|
||||||
|
|
||||||
|
#include "savetestfixture.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include <ftw.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static char_t SAVE_TEST_FIXTURE_HOME[PATH_MAX];
|
||||||
|
static char_t SAVE_TEST_FIXTURE_REAL_HOME[PATH_MAX];
|
||||||
|
static bool_t SAVE_TEST_FIXTURE_HAD_REAL_HOME;
|
||||||
|
|
||||||
|
static int_t saveTestFixtureRemoveEntry(
|
||||||
|
const char *path,
|
||||||
|
const struct stat *statBuffer,
|
||||||
|
int typeFlag,
|
||||||
|
struct FTW *ftwBuffer
|
||||||
|
) {
|
||||||
|
return remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
int saveTestFixtureSetup(void **state) {
|
||||||
|
const char_t *realHome = getenv("HOME");
|
||||||
|
SAVE_TEST_FIXTURE_HAD_REAL_HOME = realHome != NULL;
|
||||||
|
if(realHome != NULL) {
|
||||||
|
stringCopy(
|
||||||
|
SAVE_TEST_FIXTURE_REAL_HOME, realHome, sizeof(SAVE_TEST_FIXTURE_REAL_HOME) - 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stringCopy(
|
||||||
|
SAVE_TEST_FIXTURE_HOME, "/tmp/dusk-save-test-XXXXXX",
|
||||||
|
sizeof(SAVE_TEST_FIXTURE_HOME) - 1
|
||||||
|
);
|
||||||
|
assertNotNull(mkdtemp(SAVE_TEST_FIXTURE_HOME), "Failed to create temp $HOME");
|
||||||
|
|
||||||
|
assertTrue(setenv("HOME", SAVE_TEST_FIXTURE_HOME, 1) == 0, "setenv failed");
|
||||||
|
|
||||||
|
errorret_t ret = saveInit();
|
||||||
|
assertTrue(errorIsOk(ret), "saveInit failed in test fixture");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int saveTestFixtureTeardown(void **state) {
|
||||||
|
nftw(SAVE_TEST_FIXTURE_HOME, saveTestFixtureRemoveEntry, 16, FTW_DEPTH | FTW_PHYS);
|
||||||
|
|
||||||
|
if(SAVE_TEST_FIXTURE_HAD_REAL_HOME) {
|
||||||
|
setenv("HOME", SAVE_TEST_FIXTURE_REAL_HOME, 1);
|
||||||
|
} else {
|
||||||
|
unsetenv("HOME");
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryZero(&SAVE, sizeof(SAVE));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveTestFixtureBlockSaveDirectory(void) {
|
||||||
|
const char_t *home = getenv("HOME");
|
||||||
|
assertNotNull(home, "Fixture $HOME must be set before blocking it");
|
||||||
|
|
||||||
|
char_t path[PATH_MAX];
|
||||||
|
stringFormat(path, sizeof(path), "%s/.dusk", home);
|
||||||
|
|
||||||
|
FILE *file = fopen(path, "w");
|
||||||
|
assertNotNull(file, "Failed to create blocking file");
|
||||||
|
fclose(file);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "save/save.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Points $HOME at a fresh, empty temporary directory and resets the global
|
||||||
|
* SAVE state via the real saveInit(). Save tests run against the real Linux
|
||||||
|
* filesystem backend (there is no fake/mockable platform layer to swap in -
|
||||||
|
* the platform hooks are compile-time macros, not function pointers), so
|
||||||
|
* this sandboxing is what keeps tests from touching the real user's
|
||||||
|
* $HOME/.dusk/saves. Matches cmocka's CMUnitTestSetup signature - pass
|
||||||
|
* directly to cmocka_unit_test_setup_teardown.
|
||||||
|
*/
|
||||||
|
int saveTestFixtureSetup(void **state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively removes the temporary $HOME directory tree created by
|
||||||
|
* saveTestFixtureSetup and restores the real $HOME. Matches cmocka's
|
||||||
|
* CMUnitTestTeardown signature - pass directly to
|
||||||
|
* cmocka_unit_test_setup_teardown.
|
||||||
|
*/
|
||||||
|
int saveTestFixtureTeardown(void **state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places a regular file where the ".dusk" directory needs to go, so a
|
||||||
|
* subsequent availability check's mkdirp() call fails with ENOTDIR instead
|
||||||
|
* of succeeding. Used to deterministically exercise the "device unavailable
|
||||||
|
* because its directory couldn't be created" path without relying on real
|
||||||
|
* filesystem permission tricks. Must be called after saveTestFixtureSetup.
|
||||||
|
*/
|
||||||
|
void saveTestFixtureBlockSaveDirectory(void);
|
||||||
@@ -0,0 +1,602 @@
|
|||||||
|
/**
|
||||||
|
* 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 "save/savedevicelinux.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_int_equal(SAVE.slotCurrent, 0xFF);
|
||||||
|
assert_false(SAVE.findingAvailableDevice);
|
||||||
|
assert_false(SAVE.noAvailableDeviceFound);
|
||||||
|
assert_int_equal(SAVE.settings.someSetting, 0);
|
||||||
|
assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_UNKNOWN);
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
assert_int_equal(SAVE.caches[i].name[0], '\0');
|
||||||
|
assert_int_equal(SAVE.caches[i].playerLevel, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 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.someSetting = 123;
|
||||||
|
|
||||||
|
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_int_equal(onDisk.someSetting, 0);// 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.someSetting = 55;
|
||||||
|
|
||||||
|
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_int_equal(onDisk.someSetting, 55);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.someSetting = 99;
|
||||||
|
errorret_t writeRet = saveDeviceSettingsWrite(&SAVE.devices[0], &onDisk);
|
||||||
|
assert_true(errorIsOk(writeRet));
|
||||||
|
|
||||||
|
SAVE.settingsDirty = true;
|
||||||
|
SAVE.settings.someSetting = 0;
|
||||||
|
|
||||||
|
errorret_t ret = saveLoadSettings();
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_int_equal(SAVE.settings.someSetting, 99);
|
||||||
|
assert_false(SAVE.settingsDirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// saveSaveSlot / saveLoadSlot
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_saveSaveSlot_noCurrentDeviceAsserts(void **state) {
|
||||||
|
expect_assert_failure(saveSaveSlot());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSaveSlot_invalidSlotIndexAsserts(void **state) {
|
||||||
|
makeDeviceAvailable();
|
||||||
|
// slotCurrent is still 0xFF (unset) from saveInit().
|
||||||
|
expect_assert_failure(saveSaveSlot());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSaveSlot_success(void **state) {
|
||||||
|
makeDeviceAvailable();
|
||||||
|
SAVE.slotCurrent = 1;
|
||||||
|
saveSlotInit(&SAVE.slot);
|
||||||
|
stringCopy(SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name));
|
||||||
|
SAVE.slot.cachedData.playerLevel = 12;
|
||||||
|
SAVE.slotDirty = true;
|
||||||
|
|
||||||
|
errorret_t ret = saveSaveSlot();
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_false(SAVE.slotDirty);
|
||||||
|
assert_true(stringEquals(SAVE.caches[1].name, "Hero"));
|
||||||
|
assert_int_equal(SAVE.caches[1].playerLevel, 12);
|
||||||
|
|
||||||
|
saveslot_t onDisk;
|
||||||
|
saveSlotInit(&onDisk);
|
||||||
|
errorret_t readRet = saveDeviceSlotRead(&SAVE.devices[0], &onDisk, 1);
|
||||||
|
assert_true(errorIsOk(readRet));
|
||||||
|
assert_true(stringEquals(onDisk.cachedData.name, "Hero"));
|
||||||
|
assert_int_equal(onDisk.cachedData.playerLevel, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveLoadSlot_noCurrentDeviceAsserts(void **state) {
|
||||||
|
expect_assert_failure(saveLoadSlot());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveLoadSlot_invalidSlotIndexAsserts(void **state) {
|
||||||
|
makeDeviceAvailable();
|
||||||
|
expect_assert_failure(saveLoadSlot());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveLoadSlot_success(void **state) {
|
||||||
|
makeDeviceAvailable();
|
||||||
|
|
||||||
|
saveslot_t onDisk;
|
||||||
|
saveSlotInit(&onDisk);
|
||||||
|
stringCopy(onDisk.cachedData.name, "Zelda", sizeof(onDisk.cachedData.name));
|
||||||
|
onDisk.cachedData.playerLevel = 30;
|
||||||
|
errorret_t writeRet = saveDeviceSlotWrite(&SAVE.devices[0], &onDisk, 2);
|
||||||
|
assert_true(errorIsOk(writeRet));
|
||||||
|
|
||||||
|
SAVE.slotCurrent = 2;
|
||||||
|
saveSlotInit(&SAVE.slot);
|
||||||
|
SAVE.slotDirty = true;
|
||||||
|
|
||||||
|
errorret_t ret = saveLoadSlot();
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_false(SAVE.slotDirty);
|
||||||
|
assert_true(stringEquals(SAVE.slot.cachedData.name, "Zelda"));
|
||||||
|
assert_int_equal(SAVE.slot.cachedData.playerLevel, 30);
|
||||||
|
assert_true(stringEquals(SAVE.caches[2].name, "Zelda"));
|
||||||
|
assert_int_equal(SAVE.caches[2].playerLevel, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveLoadSlot_deviceReadFails_cacheStaysStale(void **state) {
|
||||||
|
makeDeviceAvailable();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
SAVE.slotCurrent = 0;
|
||||||
|
saveSlotInit(&SAVE.slot);
|
||||||
|
SAVE.caches[0].playerLevel = 77;// sentinel
|
||||||
|
|
||||||
|
errorret_t ret = saveLoadSlot();
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
|
||||||
|
// The device read failed before the cache-sync line ever ran.
|
||||||
|
assert_int_equal(SAVE.caches[0].playerLevel, 77);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// saveLoadAllSlots
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_saveLoadAllSlots_loadsAllAndEndsAtLastIndex(void **state) {
|
||||||
|
makeDeviceAvailable();
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
SAVE.slotCurrent = i;
|
||||||
|
saveSlotInit(&SAVE.slot);
|
||||||
|
SAVE.slot.cachedData.playerLevel = (int32_t)(i + 1);
|
||||||
|
errorret_t saveRet = saveSaveSlot();
|
||||||
|
assert_true(errorIsOk(saveRet));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t ret = saveLoadAllSlots();
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_int_equal(SAVE.slotCurrent, SAVE_SLOT_COUNT - 1);
|
||||||
|
|
||||||
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
|
assert_int_equal(SAVE.caches[i].playerLevel, (int32_t)(i + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveLoadAllSlots_missingFileLeavesInitDefaults(void **state) {
|
||||||
|
makeDeviceAvailable();
|
||||||
|
|
||||||
|
SAVE.slotCurrent = 0;
|
||||||
|
saveSlotInit(&SAVE.slot);
|
||||||
|
SAVE.slot.cachedData.playerLevel = 5;
|
||||||
|
errorret_t saveRet = saveSaveSlot();
|
||||||
|
assert_true(errorIsOk(saveRet));
|
||||||
|
// Slots 1 and 2 are never written - no file exists for them.
|
||||||
|
|
||||||
|
errorret_t ret = saveLoadAllSlots();
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
|
||||||
|
assert_int_equal(SAVE.caches[0].playerLevel, 5);
|
||||||
|
// Missing-file slots fall back to saveSlotInit()'s default (0), not the
|
||||||
|
// JSON-missing-field default of 1 that a corrupt-but-present file with no
|
||||||
|
// playerLevel key would produce - these are two different defaults.
|
||||||
|
assert_int_equal(SAVE.caches[1].playerLevel, 0);
|
||||||
|
assert_int_equal(SAVE.caches[2].playerLevel, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveLoadAllSlots_middleSlotCorrupt_stopsAtFailingIndex(
|
||||||
|
void **state
|
||||||
|
) {
|
||||||
|
makeDeviceAvailable();
|
||||||
|
|
||||||
|
SAVE.slotCurrent = 0;
|
||||||
|
saveSlotInit(&SAVE.slot);
|
||||||
|
errorret_t saveRet = saveSaveSlot();
|
||||||
|
assert_true(errorIsOk(saveRet));
|
||||||
|
|
||||||
|
char_t slotPath[512];
|
||||||
|
errorret_t pathRet = saveDeviceLinuxGetSlotPath(slotPath, sizeof(slotPath), 1);
|
||||||
|
assert_true(errorIsOk(pathRet));
|
||||||
|
FILE *file = fopen(slotPath, "w");
|
||||||
|
assert_non_null(file);
|
||||||
|
fprintf(file, "not valid json {{{");
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
errorret_t ret = saveLoadAllSlots();
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
|
||||||
|
// The loop bails out as soon as slot 1 fails - it never reaches slot 2.
|
||||||
|
assert_int_equal(SAVE.slotCurrent, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 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.someSetting = 321;
|
||||||
|
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_int_equal(onDisk.someSetting, 321);
|
||||||
|
}
|
||||||
|
|
||||||
|
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_saveSaveSlot_noCurrentDeviceAsserts, saveTestFixtureSetup, saveTestFixtureTeardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_saveSaveSlot_invalidSlotIndexAsserts, saveTestFixtureSetup, saveTestFixtureTeardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_saveSaveSlot_success, saveTestFixtureSetup, saveTestFixtureTeardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_saveLoadSlot_noCurrentDeviceAsserts, saveTestFixtureSetup, saveTestFixtureTeardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_saveLoadSlot_invalidSlotIndexAsserts, saveTestFixtureSetup, saveTestFixtureTeardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_saveLoadSlot_success, saveTestFixtureSetup, saveTestFixtureTeardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_saveLoadSlot_deviceReadFails_cacheStaysStale,
|
||||||
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
||||||
|
),
|
||||||
|
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_saveLoadAllSlots_loadsAllAndEndsAtLastIndex,
|
||||||
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_saveLoadAllSlots_missingFileLeavesInitDefaults,
|
||||||
|
saveTestFixtureSetup, saveTestFixtureTeardown
|
||||||
|
),
|
||||||
|
cmocka_unit_test_setup_teardown(
|
||||||
|
test_saveLoadAllSlots_middleSlotCorrupt_stopsAtFailingIndex,
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
/**
|
||||||
|
* 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/saveslot.h"
|
||||||
|
#include "save/savesettings.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;
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&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;
|
||||||
|
saveSlotInit(&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);
|
||||||
|
|
||||||
|
saveslot_t written;
|
||||||
|
saveSlotInit(&written);
|
||||||
|
written.cachedData.playerLevel = 21;
|
||||||
|
|
||||||
|
errorret_t writeRet = saveDeviceSlotWrite(&device, &written, 0);
|
||||||
|
assert_true(errorIsOk(writeRet));
|
||||||
|
|
||||||
|
saveslot_t read;
|
||||||
|
saveSlotInit(&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.someSetting = 88;
|
||||||
|
|
||||||
|
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_int_equal(read.someSetting, 88);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dusktest.h"
|
||||||
|
#include "save/savesettings.h"
|
||||||
|
#include "save/savejson.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Helpers - wrap the writeInit/readInit macros (which can only be used
|
||||||
|
// inside a function returning errorret_t) so tests can call them plainly.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static errorret_t settingsToJSON(
|
||||||
|
savesettings_t *settings,
|
||||||
|
char_t **outJson,
|
||||||
|
size_t *outLen
|
||||||
|
) {
|
||||||
|
writeInit();
|
||||||
|
|
||||||
|
errorret_t writeResult = saveSettingsWriteJSON(settings, doc, object);
|
||||||
|
if(errorIsNotOk(writeResult)) {
|
||||||
|
yyjson_mut_doc_free(doc);
|
||||||
|
errorChain(writeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
*outJson = yyjson_mut_write(doc, 0, outLen);
|
||||||
|
yyjson_mut_doc_free(doc);
|
||||||
|
assertNotNull(*outJson, "Failed to write settings JSON");
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
static errorret_t settingsFromJSON(savesettings_t *settings, const char_t *json) {
|
||||||
|
readInit(json, strlen(json));
|
||||||
|
errorret_t readResult = saveSettingsReadJSON(settings, object);
|
||||||
|
yyjson_doc_free(jsonDoc);
|
||||||
|
errorChain(readResult);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// saveSettingsInit
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_saveSettingsInit_zeroesSomeSetting(void **state) {
|
||||||
|
savesettings_t settings;
|
||||||
|
memoryZero(&settings, sizeof(settings));
|
||||||
|
settings.someSetting = 42;
|
||||||
|
|
||||||
|
saveSettingsInit(&settings);
|
||||||
|
|
||||||
|
assert_int_equal(settings.someSetting, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSettingsInit_nullAsserts(void **state) {
|
||||||
|
expect_assert_failure(saveSettingsInit(NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// saveSettingsWriteJSON / saveSettingsReadJSON
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_saveSettingsWriteJSON_nullAsserts(void **state) {
|
||||||
|
savesettings_t settings;
|
||||||
|
saveSettingsInit(&settings);
|
||||||
|
writeInit();
|
||||||
|
|
||||||
|
expect_assert_failure(saveSettingsWriteJSON(NULL, doc, object));
|
||||||
|
expect_assert_failure(saveSettingsWriteJSON(&settings, NULL, object));
|
||||||
|
expect_assert_failure(saveSettingsWriteJSON(&settings, doc, NULL));
|
||||||
|
|
||||||
|
yyjson_mut_doc_free(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSettingsReadJSON_nullAsserts(void **state) {
|
||||||
|
savesettings_t settings;
|
||||||
|
saveSettingsInit(&settings);
|
||||||
|
writeInit();
|
||||||
|
yyjson_mut_obj_add_int(doc, object, "someSetting", 1);
|
||||||
|
|
||||||
|
size_t len;
|
||||||
|
char_t *json = yyjson_mut_write(doc, 0, &len);
|
||||||
|
yyjson_mut_doc_free(doc);
|
||||||
|
assert_non_null(json);
|
||||||
|
|
||||||
|
yyjson_doc *readDoc = yyjson_read(json, len, 0);
|
||||||
|
yyjson_val *readObject = yyjson_doc_get_root(readDoc);
|
||||||
|
|
||||||
|
expect_assert_failure(saveSettingsReadJSON(NULL, readObject));
|
||||||
|
expect_assert_failure(saveSettingsReadJSON(&settings, NULL));
|
||||||
|
|
||||||
|
yyjson_doc_free(readDoc);
|
||||||
|
free(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSettingsWriteReadJSON_roundTrip(void **state) {
|
||||||
|
savesettings_t written;
|
||||||
|
saveSettingsInit(&written);
|
||||||
|
written.someSetting = 12345;
|
||||||
|
|
||||||
|
char_t *json;
|
||||||
|
size_t len;
|
||||||
|
errorret_t writeRet = settingsToJSON(&written, &json, &len);
|
||||||
|
assert_true(errorIsOk(writeRet));
|
||||||
|
|
||||||
|
savesettings_t read;
|
||||||
|
saveSettingsInit(&read);
|
||||||
|
read.someSetting = -1;// sentinel, should be overwritten
|
||||||
|
|
||||||
|
errorret_t readRet = settingsFromJSON(&read, json);
|
||||||
|
assert_true(errorIsOk(readRet));
|
||||||
|
assert_int_equal(read.someSetting, 12345);
|
||||||
|
|
||||||
|
free(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSettingsReadJSON_missingKeyDefaultsToZero(void **state) {
|
||||||
|
savesettings_t settings;
|
||||||
|
saveSettingsInit(&settings);
|
||||||
|
settings.someSetting = 999;// sentinel
|
||||||
|
|
||||||
|
errorret_t ret = settingsFromJSON(&settings, "{}");
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_int_equal(settings.someSetting, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
assertInit();
|
||||||
|
const struct CMUnitTest tests[] = {
|
||||||
|
cmocka_unit_test(test_saveSettingsInit_zeroesSomeSetting),
|
||||||
|
cmocka_unit_test(test_saveSettingsInit_nullAsserts),
|
||||||
|
|
||||||
|
cmocka_unit_test(test_saveSettingsWriteJSON_nullAsserts),
|
||||||
|
cmocka_unit_test(test_saveSettingsReadJSON_nullAsserts),
|
||||||
|
cmocka_unit_test(test_saveSettingsWriteReadJSON_roundTrip),
|
||||||
|
cmocka_unit_test(test_saveSettingsReadJSON_missingKeyDefaultsToZero),
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dusktest.h"
|
||||||
|
#include "save/saveslot.h"
|
||||||
|
#include "save/savejson.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Helpers - wrap the writeInit/readInit macros (which can only be used
|
||||||
|
// inside a function returning errorret_t) so tests can call them plainly.
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static errorret_t slotToJSON(saveslot_t *slot, char_t **outJson, size_t *outLen) {
|
||||||
|
writeInit();
|
||||||
|
|
||||||
|
errorret_t writeResult = saveSlotWriteJSON(slot, doc, object);
|
||||||
|
if(errorIsNotOk(writeResult)) {
|
||||||
|
yyjson_mut_doc_free(doc);
|
||||||
|
errorChain(writeResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
*outJson = yyjson_mut_write(doc, 0, outLen);
|
||||||
|
yyjson_mut_doc_free(doc);
|
||||||
|
assertNotNull(*outJson, "Failed to write slot JSON");
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
static errorret_t slotFromJSON(saveslot_t *slot, const char_t *json) {
|
||||||
|
readInit(json, strlen(json));
|
||||||
|
errorret_t readResult = saveSlotReadJSON(slot, object);
|
||||||
|
yyjson_doc_free(jsonDoc);
|
||||||
|
errorChain(readResult);
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// saveSlotInit
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_saveSlotInit_defaults(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
memorySet(&slot, 0xFF, sizeof(slot));
|
||||||
|
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
assert_int_equal(slot.version, 1);
|
||||||
|
assert_int_equal(slot.dataType, 0);
|
||||||
|
assert_int_equal(slot.cachedData.name[0], '\0');
|
||||||
|
assert_true(slot.cachedData.time.time == 0.0);
|
||||||
|
assert_int_equal(slot.cachedData.playerLevel, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSlotInit_nullAsserts(void **state) {
|
||||||
|
expect_assert_failure(saveSlotInit(NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// saveSlotInUse / saveSlotHasSaved
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_saveSlotInUse(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
assert_false(saveSlotInUse(&slot.cachedData));
|
||||||
|
|
||||||
|
stringCopy(slot.cachedData.name, "Hero", sizeof(slot.cachedData.name));
|
||||||
|
assert_true(saveSlotInUse(&slot.cachedData));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSlotInUse_nullAsserts(void **state) {
|
||||||
|
expect_assert_failure(saveSlotInUse(NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSlotHasSaved(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
assert_false(saveSlotHasSaved(&slot.cachedData));
|
||||||
|
|
||||||
|
// Writing to JSON stamps the current time as a side effect, so even a
|
||||||
|
// pure serialize (no device write) flips "has ever saved" to true.
|
||||||
|
char_t *json;
|
||||||
|
size_t len;
|
||||||
|
errorret_t ret = slotToJSON(&slot, &json, &len);
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_true(saveSlotHasSaved(&slot.cachedData));
|
||||||
|
|
||||||
|
free(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSlotHasSaved_nullAsserts(void **state) {
|
||||||
|
expect_assert_failure(saveSlotHasSaved(NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// saveSlotWriteJSON
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_saveSlotWriteJSON_nullAsserts(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
writeInit();
|
||||||
|
|
||||||
|
expect_assert_failure(saveSlotWriteJSON(NULL, doc, object));
|
||||||
|
expect_assert_failure(saveSlotWriteJSON(&slot, NULL, object));
|
||||||
|
expect_assert_failure(saveSlotWriteJSON(&slot, doc, NULL));
|
||||||
|
|
||||||
|
yyjson_mut_doc_free(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// saveSlotReadJSON
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_saveSlotReadJSON_nullAsserts(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
yyjson_doc *readDoc = yyjson_read("{}", 2, 0);
|
||||||
|
yyjson_val *readObject = yyjson_doc_get_root(readDoc);
|
||||||
|
|
||||||
|
expect_assert_failure(saveSlotReadJSON(NULL, readObject));
|
||||||
|
expect_assert_failure(saveSlotReadJSON(&slot, NULL));
|
||||||
|
|
||||||
|
yyjson_doc_free(readDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSlotReadJSON_roundTrip(void **state) {
|
||||||
|
saveslot_t written;
|
||||||
|
saveSlotInit(&written);
|
||||||
|
written.version = 7;// deliberately non-default, not part of the JSON schema
|
||||||
|
stringCopy(written.cachedData.name, "Hero", sizeof(written.cachedData.name));
|
||||||
|
written.cachedData.playerLevel = 42;
|
||||||
|
|
||||||
|
char_t *json;
|
||||||
|
size_t len;
|
||||||
|
errorret_t writeRet = slotToJSON(&written, &json, &len);
|
||||||
|
assert_true(errorIsOk(writeRet));
|
||||||
|
|
||||||
|
saveslot_t read;
|
||||||
|
saveSlotInit(&read);
|
||||||
|
|
||||||
|
errorret_t readRet = slotFromJSON(&read, json);
|
||||||
|
assert_true(errorIsOk(readRet));
|
||||||
|
|
||||||
|
assert_true(stringEquals(read.cachedData.name, "Hero"));
|
||||||
|
assert_int_equal(read.cachedData.playerLevel, 42);
|
||||||
|
assert_true(read.cachedData.time.time == written.cachedData.time.time);
|
||||||
|
|
||||||
|
// version/dataType are struct-only bookkeeping, never serialized - the
|
||||||
|
// reader keeps whatever saveSlotInit gave it regardless of the writer's
|
||||||
|
// version.
|
||||||
|
assert_int_equal(read.version, 1);
|
||||||
|
|
||||||
|
free(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSlotReadJSON_missingFieldsUseDefaults(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
stringCopy(slot.cachedData.name, "sentinel", sizeof(slot.cachedData.name));
|
||||||
|
slot.cachedData.playerLevel = 999;
|
||||||
|
|
||||||
|
errorret_t ret = slotFromJSON(
|
||||||
|
&slot, "{\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||||
|
);
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
|
||||||
|
assert_int_equal(slot.cachedData.name[0], '\0');
|
||||||
|
assert_int_equal(slot.cachedData.playerLevel, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSlotReadJSON_missingTimeKey_errors(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
errorret_t ret = slotFromJSON(&slot, "{\"name\":\"Hero\",\"playerLevel\":5}");
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSlotReadJSON_nameTooLong_errors(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
errorret_t ret = slotFromJSON(
|
||||||
|
&slot,
|
||||||
|
"{\"name\":\"WayTooLongAName\","
|
||||||
|
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||||
|
);
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSlotReadJSON_nonObjectRoot_errors(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
// A non-object root has no keys, so every field falls back to its
|
||||||
|
// default - except "time", which is not optional and errors instead.
|
||||||
|
errorret_t ret = slotFromJSON(&slot, "[]");
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
assertInit();
|
||||||
|
const struct CMUnitTest tests[] = {
|
||||||
|
cmocka_unit_test(test_saveSlotInit_defaults),
|
||||||
|
cmocka_unit_test(test_saveSlotInit_nullAsserts),
|
||||||
|
|
||||||
|
cmocka_unit_test(test_saveSlotInUse),
|
||||||
|
cmocka_unit_test(test_saveSlotInUse_nullAsserts),
|
||||||
|
cmocka_unit_test(test_saveSlotHasSaved),
|
||||||
|
cmocka_unit_test(test_saveSlotHasSaved_nullAsserts),
|
||||||
|
|
||||||
|
cmocka_unit_test(test_saveSlotWriteJSON_nullAsserts),
|
||||||
|
|
||||||
|
cmocka_unit_test(test_saveSlotReadJSON_nullAsserts),
|
||||||
|
cmocka_unit_test(test_saveSlotReadJSON_roundTrip),
|
||||||
|
cmocka_unit_test(test_saveSlotReadJSON_missingFieldsUseDefaults),
|
||||||
|
cmocka_unit_test(test_saveSlotReadJSON_missingTimeKey_errors),
|
||||||
|
cmocka_unit_test(test_saveSlotReadJSON_nameTooLong_errors),
|
||||||
|
cmocka_unit_test(test_saveSlotReadJSON_nonObjectRoot_errors),
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user