Add versioned save slot/settings schemas with strict version checks
Splits saveslot_t/savesettings_t into a stable, version-independent in-memory struct (save/slot/, save/settings/) and a separate frozen per-schema-version struct (saveslotver1_t/savesettingsver1_t) used only for JSON serialization, converted between manually. Every write now stamps a "version" field (SAVE_SLOT_CURRENT/SAVE_SETTINGS_CURRENT), and every read requires it to match exactly, rejecting anything else - laying the groundwork for a real migration path instead of silently misreading old/incompatible save data.
This commit is contained in:
+10
-10
@@ -57,7 +57,7 @@ static void test_saveInit_setsDefaults(void **state) {
|
||||
assert_int_equal(SAVE.slotCurrent, 0xFF);
|
||||
assert_false(SAVE.findingAvailableDevice);
|
||||
assert_false(SAVE.noAvailableDeviceFound);
|
||||
assert_int_equal(SAVE.settings.someSetting, 0);
|
||||
assert_true(SAVE.settings.audioMasterVolume == 1.0f);
|
||||
assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_UNKNOWN);
|
||||
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
@@ -229,7 +229,7 @@ static void test_saveSaveSettings_notDirty_isNoop(void **state) {
|
||||
// if someone changes the no-op condition without meaning to.
|
||||
makeDeviceAvailable();
|
||||
assert_false(SAVE.settingsDirty);
|
||||
SAVE.settings.someSetting = 123;
|
||||
SAVE.settings.audioMasterVolume = 12.0f;
|
||||
|
||||
errorret_t ret = saveSaveSettings();
|
||||
assert_true(errorIsOk(ret));
|
||||
@@ -238,7 +238,7 @@ static void test_saveSaveSettings_notDirty_isNoop(void **state) {
|
||||
saveSettingsInit(&onDisk);
|
||||
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
||||
assert_true(errorIsOk(readRet));
|
||||
assert_int_equal(onDisk.someSetting, 0);// never actually written
|
||||
assert_true(onDisk.audioMasterVolume == 1.0f);// never actually written
|
||||
}
|
||||
|
||||
static void test_saveSaveSettings_dirtyForced_writesAndClearsFlag(
|
||||
@@ -246,7 +246,7 @@ static void test_saveSaveSettings_dirtyForced_writesAndClearsFlag(
|
||||
) {
|
||||
makeDeviceAvailable();
|
||||
SAVE.settingsDirty = true;// nothing in real code sets this - forced here
|
||||
SAVE.settings.someSetting = 55;
|
||||
SAVE.settings.audioMasterVolume = 5.5f;
|
||||
|
||||
errorret_t ret = saveSaveSettings();
|
||||
assert_true(errorIsOk(ret));
|
||||
@@ -256,7 +256,7 @@ static void test_saveSaveSettings_dirtyForced_writesAndClearsFlag(
|
||||
saveSettingsInit(&onDisk);
|
||||
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
||||
assert_true(errorIsOk(readRet));
|
||||
assert_int_equal(onDisk.someSetting, 55);
|
||||
assert_true(onDisk.audioMasterVolume == 5.5f);
|
||||
}
|
||||
|
||||
static void test_saveLoadSettings_noCurrentDeviceAsserts(void **state) {
|
||||
@@ -268,16 +268,16 @@ static void test_saveLoadSettings_readsAndClearsDirtyFlag(void **state) {
|
||||
|
||||
savesettings_t onDisk;
|
||||
saveSettingsInit(&onDisk);
|
||||
onDisk.someSetting = 99;
|
||||
onDisk.audioMasterVolume = 9.25f;
|
||||
errorret_t writeRet = saveDeviceSettingsWrite(&SAVE.devices[0], &onDisk);
|
||||
assert_true(errorIsOk(writeRet));
|
||||
|
||||
SAVE.settingsDirty = true;
|
||||
SAVE.settings.someSetting = 0;
|
||||
SAVE.settings.audioMasterVolume = 0.0f;
|
||||
|
||||
errorret_t ret = saveLoadSettings();
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal(SAVE.settings.someSetting, 99);
|
||||
assert_true(SAVE.settings.audioMasterVolume == 9.25f);
|
||||
assert_false(SAVE.settingsDirty);
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ static void test_saveUpdate_foundDevice_rewritesSettingsUnconditionally(
|
||||
// it, regardless of settingsDirty - independent of (and unguarded by) the
|
||||
// same dirty-flag check saveSaveSettings() itself honors.
|
||||
resetFindCallbackState();
|
||||
SAVE.settings.someSetting = 321;
|
||||
SAVE.settings.audioMasterVolume = 3.25f;
|
||||
assert_false(SAVE.settingsDirty);
|
||||
|
||||
saveFindAvailableDevice(findAvailableCallback, NULL);
|
||||
@@ -482,7 +482,7 @@ static void test_saveUpdate_foundDevice_rewritesSettingsUnconditionally(
|
||||
saveSettingsInit(&onDisk);
|
||||
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
||||
assert_true(errorIsOk(readRet));
|
||||
assert_int_equal(onDisk.someSetting, 321);
|
||||
assert_true(onDisk.audioMasterVolume == 3.25f);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#include "dusktest.h"
|
||||
#include "savetestfixture.h"
|
||||
#include "save/savedevice.h"
|
||||
#include "save/saveslot.h"
|
||||
#include "save/savesettings.h"
|
||||
#include "save/slot/saveslot.h"
|
||||
#include "save/settings/savesettings.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
@@ -224,7 +224,7 @@ static void test_saveDeviceSettings_dispatchesToPlatform(void **state) {
|
||||
|
||||
savesettings_t written;
|
||||
saveSettingsInit(&written);
|
||||
written.someSetting = 88;
|
||||
written.audioMasterVolume = 0.25f;// exactly representable, safe to compare
|
||||
|
||||
errorret_t writeRet = saveDeviceSettingsWrite(&device, &written);
|
||||
assert_true(errorIsOk(writeRet));
|
||||
@@ -234,7 +234,7 @@ static void test_saveDeviceSettings_dispatchesToPlatform(void **state) {
|
||||
errorret_t readRet = saveDeviceSettingsRead(&device, &read);
|
||||
assert_true(errorIsOk(readRet));
|
||||
|
||||
assert_int_equal(read.someSetting, 88);
|
||||
assert_true(read.audioMasterVolume == 0.25f);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
#include "savetestfixture.h"
|
||||
#include "save/savedevice.h"
|
||||
#include "save/savedevicelinux.h"
|
||||
#include "save/saveslot.h"
|
||||
#include "save/savesettings.h"
|
||||
#include "save/slot/saveslot.h"
|
||||
#include "save/settings/savesettings.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include <stdio.h>
|
||||
@@ -298,7 +298,7 @@ static void test_settingsWriteRead_roundTrip(void **state) {
|
||||
|
||||
savesettings_t written;
|
||||
saveSettingsInit(&written);
|
||||
written.someSetting = 314;
|
||||
written.audioMasterVolume = 3.25f;// exactly representable, safe to compare
|
||||
|
||||
errorret_t writeRet = saveDeviceLinuxSettingsWrite(&device, &written);
|
||||
assert_true(errorIsOk(writeRet));
|
||||
@@ -308,7 +308,7 @@ static void test_settingsWriteRead_roundTrip(void **state) {
|
||||
|
||||
errorret_t readRet = saveDeviceLinuxSettingsRead(&device, &read);
|
||||
assert_true(errorIsOk(readRet));
|
||||
assert_int_equal(read.someSetting, 314);
|
||||
assert_true(read.audioMasterVolume == 3.25f);
|
||||
}
|
||||
|
||||
static void test_settingsRead_noFileYet_leavesUntouched(void **state) {
|
||||
@@ -317,11 +317,11 @@ static void test_settingsRead_noFileYet_leavesUntouched(void **state) {
|
||||
|
||||
savesettings_t settings;
|
||||
saveSettingsInit(&settings);
|
||||
settings.someSetting = 777;
|
||||
settings.audioMasterVolume = 7.75f;
|
||||
|
||||
errorret_t ret = saveDeviceLinuxSettingsRead(&device, &settings);
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_int_equal(settings.someSetting, 777);
|
||||
assert_true(settings.audioMasterVolume == 7.75f);
|
||||
}
|
||||
|
||||
static void test_settingsRead_corruptFile_errors(void **state) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "save/savesettings.h"
|
||||
#include "save/settings/savesettings.h"
|
||||
#include "save/savejson.h"
|
||||
#include "util/memory.h"
|
||||
#include <stdlib.h>
|
||||
@@ -51,14 +51,14 @@ static errorret_t settingsFromJSON(
|
||||
// saveSettingsInit
|
||||
// ============================================================
|
||||
|
||||
static void test_saveSettingsInit_zeroesSomeSetting(void **state) {
|
||||
static void test_saveSettingsInit_resetsAudioMasterVolume(void **state) {
|
||||
savesettings_t settings;
|
||||
memoryZero(&settings, sizeof(settings));
|
||||
settings.someSetting = 42;
|
||||
settings.audioMasterVolume = 42.0f;
|
||||
|
||||
saveSettingsInit(&settings);
|
||||
|
||||
assert_int_equal(settings.someSetting, 0);
|
||||
assert_true(settings.audioMasterVolume == 1.0f);
|
||||
}
|
||||
|
||||
static void test_saveSettingsInit_nullAsserts(void **state) {
|
||||
@@ -85,7 +85,6 @@ 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);
|
||||
@@ -105,7 +104,7 @@ static void test_saveSettingsReadJSON_nullAsserts(void **state) {
|
||||
static void test_saveSettingsWriteReadJSON_roundTrip(void **state) {
|
||||
savesettings_t written;
|
||||
saveSettingsInit(&written);
|
||||
written.someSetting = 12345;
|
||||
written.audioMasterVolume = 2.5f;
|
||||
|
||||
char_t *json;
|
||||
size_t len;
|
||||
@@ -114,35 +113,59 @@ static void test_saveSettingsWriteReadJSON_roundTrip(void **state) {
|
||||
|
||||
savesettings_t read;
|
||||
saveSettingsInit(&read);
|
||||
read.someSetting = -1;// sentinel, should be overwritten
|
||||
read.audioMasterVolume = -1.0f;// sentinel, should be overwritten
|
||||
|
||||
errorret_t readRet = settingsFromJSON(&read, json);
|
||||
assert_true(errorIsOk(readRet));
|
||||
assert_int_equal(read.someSetting, 12345);
|
||||
assert_true(read.audioMasterVolume == 2.5f);
|
||||
|
||||
free(json);
|
||||
}
|
||||
|
||||
static void test_saveSettingsReadJSON_missingKeyDefaultsToZero(void **state) {
|
||||
static void test_saveSettingsReadJSON_missingKeyUsesDefault(void **state) {
|
||||
savesettings_t settings;
|
||||
saveSettingsInit(&settings);
|
||||
settings.audioMasterVolume = 999.0f;// sentinel
|
||||
|
||||
errorret_t ret = settingsFromJSON(&settings, "{\"version\":1}");
|
||||
assert_true(errorIsOk(ret));
|
||||
assert_true(settings.audioMasterVolume == 1.0f);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// saveSettingsReadJSON - version enforcement
|
||||
// ============================================================
|
||||
|
||||
static void test_saveSettingsReadJSON_versionMissing_errors(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);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
}
|
||||
|
||||
static void test_saveSettingsReadJSON_versionMismatch_errors(void **state) {
|
||||
savesettings_t settings;
|
||||
saveSettingsInit(&settings);
|
||||
|
||||
errorret_t ret = settingsFromJSON(&settings, "{\"version\":999}");
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_saveSettingsInit_zeroesSomeSetting),
|
||||
cmocka_unit_test(test_saveSettingsInit_resetsAudioMasterVolume),
|
||||
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),
|
||||
cmocka_unit_test(test_saveSettingsReadJSON_missingKeyUsesDefault),
|
||||
cmocka_unit_test(test_saveSettingsReadJSON_versionMissing_errors),
|
||||
cmocka_unit_test(test_saveSettingsReadJSON_versionMismatch_errors),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "dusktest.h"
|
||||
#include "save/saveslot.h"
|
||||
#include "save/slot/saveslot.h"
|
||||
#include "save/savejson.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
@@ -174,7 +174,9 @@ static void test_saveSlotReadJSON_missingFieldsUseDefaults(void **state) {
|
||||
slot.cachedData.playerLevel = 999;
|
||||
|
||||
errorret_t ret = slotFromJSON(
|
||||
&slot, "{\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||
&slot,
|
||||
"{\"version\":1,"
|
||||
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||
);
|
||||
assert_true(errorIsOk(ret));
|
||||
|
||||
@@ -186,7 +188,9 @@ static void test_saveSlotReadJSON_missingTimeKey_errors(void **state) {
|
||||
saveslot_t slot;
|
||||
saveSlotInit(&slot);
|
||||
|
||||
errorret_t ret = slotFromJSON(&slot, "{\"name\":\"Hero\",\"playerLevel\":5}");
|
||||
errorret_t ret = slotFromJSON(
|
||||
&slot, "{\"version\":1,\"name\":\"Hero\",\"playerLevel\":5}"
|
||||
);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
}
|
||||
@@ -197,7 +201,7 @@ static void test_saveSlotReadJSON_nameTooLong_errors(void **state) {
|
||||
|
||||
errorret_t ret = slotFromJSON(
|
||||
&slot,
|
||||
"{\"name\":\"WayTooLongAName\","
|
||||
"{\"version\":1,\"name\":\"WayTooLongAName\","
|
||||
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||
);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
@@ -209,12 +213,43 @@ static void test_saveSlotReadJSON_nonObjectRoot_errors(void **state) {
|
||||
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.
|
||||
// default - except "version" and "time", which are required and error
|
||||
// instead.
|
||||
errorret_t ret = slotFromJSON(&slot, "[]");
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// saveSlotReadJSON - version enforcement
|
||||
// ============================================================
|
||||
|
||||
static void test_saveSlotReadJSON_versionMissing_errors(void **state) {
|
||||
saveslot_t slot;
|
||||
saveSlotInit(&slot);
|
||||
|
||||
errorret_t ret = slotFromJSON(
|
||||
&slot,
|
||||
"{\"name\":\"Hero\","
|
||||
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||
);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
}
|
||||
|
||||
static void test_saveSlotReadJSON_versionMismatch_errors(void **state) {
|
||||
saveslot_t slot;
|
||||
saveSlotInit(&slot);
|
||||
|
||||
errorret_t ret = slotFromJSON(
|
||||
&slot,
|
||||
"{\"version\":999,\"name\":\"Hero\","
|
||||
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||
);
|
||||
assert_true(errorIsNotOk(ret));
|
||||
errorCatch(ret);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
assertInit();
|
||||
const struct CMUnitTest tests[] = {
|
||||
@@ -234,6 +269,8 @@ int main(void) {
|
||||
cmocka_unit_test(test_saveSlotReadJSON_missingTimeKey_errors),
|
||||
cmocka_unit_test(test_saveSlotReadJSON_nameTooLong_errors),
|
||||
cmocka_unit_test(test_saveSlotReadJSON_nonObjectRoot_errors),
|
||||
cmocka_unit_test(test_saveSlotReadJSON_versionMissing_errors),
|
||||
cmocka_unit_test(test_saveSlotReadJSON_versionMismatch_errors),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
|
||||
Reference in New Issue
Block a user