- 将 ShrinkEventBus、ShrinkDataSaver 及其 EventBus 集成从 gitlink 转为仓库直接维护的完整 UPM 包,补齐运行时、编辑器工具、测试与文档 - 新增 Command 和 Network 的 App 集成组件,支持 ContextLoader 服务发布、可逆注销及 Network Loopback 生命周期管理 - 更新 Starter 与演示组合逻辑,缺失模块时可注册、已有兼容安装器时可覆盖,并补充宿主启动断言 - 升级内部包依赖与 Shared CodeGen 包定义,放宽 Integration.App 包的 Git 忽略规则 - 将独立服务器生成器改为基于已编译程序集的语义扫描,支持 partial、复杂泛型、命名冲突检测及模板 SHA-256 覆写保护 - 新增 Network 语义扫描、模板保护和 App 组件生命周期测试 - 新增真实 UPM 消费工程验证脚本,校验内部版本一致性、程序集加载及 EditMode 测试 - 重构当前架构文档并归档已完成的 Cordis 迁移与旧代码地图
630 lines
23 KiB
C#
630 lines
23 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Cysharp.Threading.Tasks;
|
|
using Newtonsoft.Json.Linq;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.TestTools;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace ShrinkDataSaver.Tests
|
|
{
|
|
[TestFixture]
|
|
public class ShrinkSaveTests
|
|
{
|
|
private MockStorageProvider _storage;
|
|
private ShrinkDataSaverSettings _settings;
|
|
|
|
[Serializable]
|
|
private class PlayerData
|
|
{
|
|
public string Name = "TestPlayer";
|
|
public int Level = 1;
|
|
public int Coins = 100;
|
|
}
|
|
|
|
private PlayerData _playerData;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_storage = new MockStorageProvider();
|
|
_settings = ScriptableObject.CreateInstance<ShrinkDataSaverSettings>();
|
|
_settings.maxSlots = 10;
|
|
ShrinkDataSaverSettings.Instance = _settings;
|
|
|
|
ShrinkSave.ResetForTesting();
|
|
MigrationChain.Clear();
|
|
ShrinkSave.Initialize(_storage, "saves", ".sav", 1);
|
|
|
|
_playerData = new PlayerData();
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
ShrinkSave.ResetForTesting();
|
|
MigrationChain.Clear();
|
|
if (_settings != null) Object.DestroyImmediate(_settings);
|
|
}
|
|
|
|
// ── 模块注册 ──
|
|
|
|
[Test]
|
|
public void RegisterModule_Lambda()
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
Assert.IsTrue(ShrinkSave.HasModule("player"));
|
|
}
|
|
|
|
[Test]
|
|
public void RegisterModule_WithConfig()
|
|
{
|
|
var config = new ModuleConfig
|
|
{
|
|
EnableCloudSync = false,
|
|
AutoSaveIntervalSeconds = 30f,
|
|
CriticalModule = true
|
|
};
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d, config);
|
|
|
|
var restored = ShrinkSave.GetModuleConfig("player");
|
|
Assert.IsFalse(restored.EnableCloudSync);
|
|
Assert.AreEqual(30f, restored.AutoSaveIntervalSeconds);
|
|
Assert.IsTrue(restored.CriticalModule);
|
|
}
|
|
|
|
[Test]
|
|
public void UnregisterModule_RemovesModuleAndConfig()
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d,
|
|
new ModuleConfig { CriticalModule = true });
|
|
|
|
ShrinkSave.UnregisterModule("player");
|
|
Assert.IsFalse(ShrinkSave.HasModule("player"));
|
|
Assert.IsNull(ShrinkSave.GetModuleConfig("player"));
|
|
}
|
|
|
|
[Test]
|
|
public void GetModuleConfig_NonExistent_ReturnsNull()
|
|
{
|
|
Assert.IsNull(ShrinkSave.GetModuleConfig("ghost"));
|
|
}
|
|
|
|
[Test]
|
|
public void HasModule_ReturnsFalse_WhenNotRegistered()
|
|
{
|
|
Assert.IsFalse(ShrinkSave.HasModule("nonexistent"));
|
|
}
|
|
|
|
[Test]
|
|
public void GetRegisteredModuleNames_ReturnsAll()
|
|
{
|
|
ShrinkSave.RegisterModule("a", () => 1, _ => { });
|
|
ShrinkSave.RegisterModule("b", () => 2, _ => { });
|
|
|
|
var names = ShrinkSave.GetRegisteredModuleNames();
|
|
Assert.AreEqual(2, names.Count);
|
|
Assert.IsTrue(names.Contains("a"));
|
|
Assert.IsTrue(names.Contains("b"));
|
|
}
|
|
|
|
// ── 保存 / 加载 / 删除 ──
|
|
|
|
[UnityTest]
|
|
public IEnumerator SaveSlotAsync_ThenExists() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "TestSave" });
|
|
|
|
Assert.IsTrue(await ShrinkSave.SlotExistsAsync(0));
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator SaveLoad_RoundTrip_DataIntegrity() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
_playerData = new PlayerData { Name = "Alice", Level = 10, Coins = 500 };
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
|
|
await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "TestSave" });
|
|
|
|
// 清除内存数据
|
|
_playerData = new PlayerData();
|
|
Assert.AreEqual("TestPlayer", _playerData.Name);
|
|
|
|
// 重新加载
|
|
await ShrinkSave.LoadSlotAsync(0);
|
|
Assert.AreEqual("Alice", _playerData.Name);
|
|
Assert.AreEqual(10, _playerData.Level);
|
|
Assert.AreEqual(500, _playerData.Coins);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator SaveLoad_Encrypted_RoundTrip() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
_playerData = new PlayerData { Name = "Encrypted", Level = 99, Coins = 9999 };
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
|
|
await ShrinkSave.SaveSlotAsync(0, new SaveOptions
|
|
{
|
|
SlotName = "Encrypted Save",
|
|
Encrypt = true,
|
|
EncryptionKey = "secret123"
|
|
});
|
|
|
|
_playerData = new PlayerData();
|
|
await ShrinkSave.LoadSlotAsync(0, "secret123");
|
|
|
|
Assert.AreEqual("Encrypted", _playerData.Name);
|
|
Assert.AreEqual(99, _playerData.Level);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator DeleteSlotAsync_RemovesSlot() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
|
|
Assert.IsTrue(await ShrinkSave.SlotExistsAsync(0));
|
|
|
|
await ShrinkSave.DeleteSlotAsync(0);
|
|
Assert.IsFalse(await ShrinkSave.SlotExistsAsync(0));
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator LoadSlotAsync_NonExistent_ThrowsAndFiresLoadFailed() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
LoadFailedEventArgs failArgs = null;
|
|
ShrinkSave.OnLoadFailed += args => failArgs = args;
|
|
|
|
try
|
|
{
|
|
await ShrinkSave.LoadSlotAsync(99);
|
|
Assert.Fail("Should have thrown");
|
|
}
|
|
catch (System.IO.FileNotFoundException) { }
|
|
|
|
Assert.IsNotNull(failArgs);
|
|
Assert.AreEqual(99, failArgs.SlotIndex);
|
|
Assert.IsTrue(failArgs.ErrorMessage.Contains("99"));
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator SaveSlotAsync_NegativeIndex_Throws() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
try
|
|
{
|
|
await ShrinkSave.SaveSlotAsync(-1);
|
|
Assert.Fail("Should have thrown");
|
|
}
|
|
catch (ArgumentOutOfRangeException) { }
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator SaveSlotAsync_ExceedsMaxSlots_Throws() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
_settings.maxSlots = 3;
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
try
|
|
{
|
|
await ShrinkSave.SaveSlotAsync(5);
|
|
Assert.Fail("Should have thrown");
|
|
}
|
|
catch (ArgumentOutOfRangeException) { }
|
|
});
|
|
|
|
// ── 事件 ──
|
|
|
|
[UnityTest]
|
|
public IEnumerator Events_SaveStarted_FiredBeforeSave() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
|
|
SaveStartedEventArgs startArgs = null;
|
|
ShrinkSave.OnSaveStarted += args => startArgs = args;
|
|
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
|
|
Assert.IsNotNull(startArgs);
|
|
Assert.AreEqual(0, startArgs.SlotIndex);
|
|
Assert.IsTrue(startArgs.Timestamp > 0);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator Events_SaveCompleted_ContainsModuleNames() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
ShrinkSave.RegisterModule("settings", () => new { volume = 1f }, _ => { });
|
|
|
|
SaveCompletedEventArgs completedArgs = null;
|
|
ShrinkSave.OnSaveCompleted += args => completedArgs = args;
|
|
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
|
|
Assert.IsNotNull(completedArgs);
|
|
Assert.AreEqual(0, completedArgs.SlotIndex);
|
|
Assert.AreEqual(2, completedArgs.ModuleNames.Length);
|
|
Assert.IsTrue(completedArgs.Timestamp > 0);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator Events_LoadCompleted_ContainsVersionAndModules() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
|
|
LoadCompletedEventArgs loadArgs = null;
|
|
ShrinkSave.OnLoadCompleted += args => loadArgs = args;
|
|
|
|
await ShrinkSave.LoadSlotAsync(0);
|
|
|
|
Assert.IsNotNull(loadArgs);
|
|
Assert.AreEqual(0, loadArgs.SlotIndex);
|
|
Assert.AreEqual(1, loadArgs.Version);
|
|
Assert.IsTrue(loadArgs.ModuleNames.Length > 0);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator Events_LoadStarted_Fired() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
|
|
LoadStartedEventArgs startArgs = null;
|
|
ShrinkSave.OnLoadStarted += args => startArgs = args;
|
|
|
|
await ShrinkSave.LoadSlotAsync(0);
|
|
|
|
Assert.IsNotNull(startArgs);
|
|
Assert.AreEqual(0, startArgs.SlotIndex);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator Events_DeleteCompleted_Fired() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
|
|
SlotDeletedEventArgs deleteArgs = null;
|
|
ShrinkSave.OnDeleteCompleted += args => deleteArgs = args;
|
|
|
|
await ShrinkSave.DeleteSlotAsync(0);
|
|
|
|
Assert.IsNotNull(deleteArgs);
|
|
Assert.AreEqual(0, deleteArgs.SlotIndex);
|
|
});
|
|
|
|
// ── 关键模块 ──
|
|
|
|
[UnityTest]
|
|
public IEnumerator CriticalModule_FailedSerialize_AbortsSave() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
// 注册一个会抛异常的关键模块
|
|
ShrinkSave.RegisterModule<object>("broken",
|
|
() => throw new Exception("boom"),
|
|
_ => { },
|
|
new ModuleConfig { CriticalModule = true });
|
|
|
|
SaveFailedEventArgs failArgs = null;
|
|
ShrinkSave.OnSaveFailed += args => failArgs = args;
|
|
|
|
try
|
|
{
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
Assert.Fail("Should have thrown");
|
|
}
|
|
catch (InvalidOperationException) { }
|
|
|
|
Assert.IsNotNull(failArgs);
|
|
Assert.IsTrue(failArgs.ErrorMessage.Contains("broken"));
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator NonCriticalModule_FailedSerialize_ContinuesSave() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
// 正常模块
|
|
ShrinkSave.RegisterModule("good", () => _playerData, d => _playerData = d);
|
|
// 会失败的非关键模块
|
|
ShrinkSave.RegisterModule<object>("bad",
|
|
() => throw new Exception("oops"),
|
|
_ => { },
|
|
new ModuleConfig { CriticalModule = false });
|
|
|
|
SaveCompletedEventArgs completedArgs = null;
|
|
ShrinkSave.OnSaveCompleted += args => completedArgs = args;
|
|
|
|
LogAssert.Expect(LogType.Error, "[ShrinkDataSaver] 模块 'bad' 序列化失败(已跳过): oops");
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
|
|
// 保存应该成功(跳过了失败的非关键模块)
|
|
Assert.IsNotNull(completedArgs);
|
|
Assert.IsTrue(await ShrinkSave.SlotExistsAsync(0));
|
|
});
|
|
|
|
// ── 版本迁移事件 ──
|
|
|
|
[UnityTest]
|
|
public IEnumerator MigrationCompleted_FiredOnVersionMismatch() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
// 以 v1 保存
|
|
ShrinkSave.SetCurrentSaveVersion(1);
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
|
|
// 设置迁移并升级到 v2
|
|
MigrationChain.Register(1, 2, data =>
|
|
{
|
|
data["migrated"] = true;
|
|
return data;
|
|
});
|
|
ShrinkSave.SetCurrentSaveVersion(2);
|
|
|
|
MigrationCompletedEventArgs migArgs = null;
|
|
ShrinkSave.OnMigrationCompleted += args => migArgs = args;
|
|
|
|
await ShrinkSave.LoadSlotAsync(0);
|
|
|
|
Assert.IsNotNull(migArgs);
|
|
Assert.AreEqual(0, migArgs.SlotIndex);
|
|
Assert.AreEqual(1, migArgs.FromVersion);
|
|
Assert.AreEqual(2, migArgs.ToVersion);
|
|
});
|
|
|
|
// ── 跨模块查询 ──
|
|
|
|
[Test]
|
|
public void QueryModule_ReturnsCurrentData()
|
|
{
|
|
_playerData = new PlayerData { Name = "Query", Level = 5, Coins = 200 };
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
|
|
var result = ShrinkSave.QueryModule<PlayerData>("player");
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual("Query", result.Name);
|
|
Assert.AreEqual(5, result.Level);
|
|
}
|
|
|
|
[Test]
|
|
public void QueryModule_NonExistentModule_ReturnsDefault()
|
|
{
|
|
var result = ShrinkSave.QueryModule<PlayerData>("ghost");
|
|
Assert.IsNull(result);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator HasKey_AfterLoad_ChecksModuleData() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
_playerData = new PlayerData { Name = "KeyTest", Level = 1, Coins = 0 };
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
await ShrinkSave.LoadSlotAsync(0);
|
|
|
|
Assert.IsTrue(ShrinkSave.HasKey("player", "Name"));
|
|
Assert.IsTrue(ShrinkSave.HasKey("player", "Level"));
|
|
Assert.IsFalse(ShrinkSave.HasKey("player", "NonExistentField"));
|
|
Assert.IsFalse(ShrinkSave.HasKey("ghost_module", "Name"));
|
|
});
|
|
|
|
[Test]
|
|
public void HasKey_BeforeLoad_ReturnsFalse()
|
|
{
|
|
Assert.IsFalse(ShrinkSave.HasKey("player", "Name"));
|
|
}
|
|
|
|
// ── 自动保存间隔 ──
|
|
|
|
[Test]
|
|
public void GetMinAutoSaveInterval_NoModules_ReturnsZero()
|
|
{
|
|
Assert.AreEqual(0f, ShrinkSave.GetMinAutoSaveInterval());
|
|
}
|
|
|
|
[Test]
|
|
public void GetMinAutoSaveInterval_ReturnsMinimum()
|
|
{
|
|
ShrinkSave.RegisterModule("a", () => 1, _ => { },
|
|
new ModuleConfig { AutoSaveIntervalSeconds = 60f });
|
|
ShrinkSave.RegisterModule("b", () => 2, _ => { },
|
|
new ModuleConfig { AutoSaveIntervalSeconds = 30f });
|
|
ShrinkSave.RegisterModule("c", () => 3, _ => { },
|
|
new ModuleConfig { AutoSaveIntervalSeconds = 0f }); // 不参与
|
|
|
|
Assert.AreEqual(30f, ShrinkSave.GetMinAutoSaveInterval());
|
|
}
|
|
|
|
// ── 元数据 ──
|
|
|
|
[UnityTest]
|
|
public IEnumerator GetMetaAsync_ReturnsMeta() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "MetaTest" });
|
|
|
|
var meta = await ShrinkSave.GetMetaAsync(0);
|
|
Assert.IsNotNull(meta);
|
|
Assert.AreEqual(0, meta.SlotIndex);
|
|
Assert.AreEqual("MetaTest", meta.SlotName);
|
|
Assert.AreEqual(1, meta.SaveVersion);
|
|
Assert.IsFalse(meta.IsEncrypted);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator GetMetaAsync_NonExistent_ReturnsNull() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
var meta = await ShrinkSave.GetMetaAsync(99);
|
|
Assert.IsNull(meta);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator GetAllMetaAsync_ReturnsAllSlots() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "Slot0" });
|
|
await ShrinkSave.SaveSlotAsync(1, new SaveOptions { SlotName = "Slot1" });
|
|
|
|
var allMeta = await ShrinkSave.GetAllMetaAsync();
|
|
Assert.AreEqual(2, allMeta.Length);
|
|
Assert.AreEqual(0, allMeta[0].SlotIndex);
|
|
Assert.AreEqual(1, allMeta[1].SlotIndex);
|
|
});
|
|
|
|
// ── LoadedSlot ──
|
|
|
|
[Test]
|
|
public void LoadedSlot_InitiallyNegative()
|
|
{
|
|
Assert.AreEqual(-1, ShrinkSave.LoadedSlot);
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator LoadedSlot_UpdatedAfterLoad() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
await ShrinkSave.LoadSlotAsync(0);
|
|
|
|
Assert.AreEqual(0, ShrinkSave.LoadedSlot);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator LoadedSlot_ResetAfterDelete() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
await ShrinkSave.LoadSlotAsync(0);
|
|
Assert.AreEqual(0, ShrinkSave.LoadedSlot);
|
|
|
|
await ShrinkSave.DeleteSlotAsync(0);
|
|
Assert.AreEqual(-1, ShrinkSave.LoadedSlot);
|
|
});
|
|
|
|
// ── 多模块 ──
|
|
|
|
[UnityTest]
|
|
public IEnumerator MultipleModules_SaveLoad_AllRestored() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
var inventory = new int[] { 1, 2, 3 };
|
|
var questFlag = new bool[] { true, false, true };
|
|
int[] loadedInv = null;
|
|
bool[] loadedQuest = null;
|
|
|
|
ShrinkSave.RegisterModule("inventory", () => inventory, d => loadedInv = d);
|
|
ShrinkSave.RegisterModule("quests", () => questFlag, d => loadedQuest = d);
|
|
|
|
await ShrinkSave.SaveSlotAsync(0);
|
|
await ShrinkSave.LoadSlotAsync(0);
|
|
|
|
Assert.AreEqual(new[] { 1, 2, 3 }, loadedInv);
|
|
Assert.AreEqual(new[] { true, false, true }, loadedQuest);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator LoadSlotAsync_FallsBackToBackup_WhenPrimaryCorrupted() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
_playerData = new PlayerData { Name = "BackupPlayer", Level = 7, Coins = 321 };
|
|
await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "BackupSlot" });
|
|
|
|
var backupBytes = await _storage.ReadAsync("saves/slot_0.sav");
|
|
await _storage.WriteAsync("saves/slot_0.sav.bak1", backupBytes);
|
|
await _storage.WriteAsync("saves/slot_0.sav", DataSerializer.Serialize(new { broken = true }));
|
|
|
|
_playerData = new PlayerData();
|
|
await ShrinkSave.LoadSlotAsync(0);
|
|
|
|
Assert.AreEqual("BackupPlayer", _playerData.Name);
|
|
|
|
var repairedPrimary = await _storage.ReadAsync("saves/slot_0.sav");
|
|
CollectionAssert.AreEqual(backupBytes, repairedPrimary);
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator GetAllMetaAsync_UsesBackupOnlySlot_WhenPrimaryMissing() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
await ShrinkSave.SaveSlotAsync(2, new SaveOptions { SlotName = "BackupOnly" });
|
|
|
|
var backupBytes = await _storage.ReadAsync("saves/slot_2.sav");
|
|
await _storage.WriteAsync("saves/slot_2.sav.bak1", backupBytes);
|
|
await _storage.DeleteAsync("saves/slot_2.sav");
|
|
|
|
var metas = await ShrinkSave.GetAllMetaAsync();
|
|
|
|
Assert.AreEqual(1, metas.Length);
|
|
Assert.AreEqual(2, metas[0].SlotIndex);
|
|
Assert.AreEqual("BackupOnly", metas[0].SlotName);
|
|
Assert.IsTrue(await _storage.ExistsAsync("saves/slot_2.sav"));
|
|
});
|
|
|
|
[UnityTest]
|
|
public IEnumerator GetRecommendedContinueSlotAsync_PrefersRecentAndSelfHeals() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
ShrinkSave.RegisterModule("player", () => _playerData, d => _playerData = d);
|
|
|
|
_playerData = new PlayerData { Name = "Slot0", Level = 1, Coins = 10 };
|
|
await ShrinkSave.SaveSlotAsync(0, new SaveOptions { SlotName = "Slot0" });
|
|
|
|
_playerData = new PlayerData { Name = "Slot3", Level = 3, Coins = 30 };
|
|
await ShrinkSave.SaveSlotAsync(3, new SaveOptions { SlotName = "Slot3" });
|
|
|
|
await ShrinkSettings.LoadAsync();
|
|
ShrinkSettings.Set("ShrinkDataSaver.RecentSlotIndex", 3);
|
|
await ShrinkSettings.SaveAsync();
|
|
|
|
var preferred = await ShrinkSave.GetRecommendedContinueSlotAsync();
|
|
Assert.AreEqual(3, preferred);
|
|
|
|
await ShrinkSave.DeleteSlotAsync(3);
|
|
preferred = await ShrinkSave.GetRecommendedContinueSlotAsync();
|
|
Assert.AreEqual(0, preferred);
|
|
Assert.AreEqual(0, ShrinkSave.GetRecentSlotIndex());
|
|
});
|
|
}
|
|
|
|
[TestFixture]
|
|
public class LocalStorageProviderTests
|
|
{
|
|
private string _rootPath;
|
|
private LocalStorageProvider _provider;
|
|
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_rootPath = Path.Combine(Path.GetTempPath(), $"ShrinkDataSaverTests_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_rootPath);
|
|
_provider = new LocalStorageProvider(_rootPath);
|
|
}
|
|
|
|
[TearDown]
|
|
public void TearDown()
|
|
{
|
|
if (Directory.Exists(_rootPath))
|
|
{
|
|
Directory.Delete(_rootPath, true);
|
|
}
|
|
}
|
|
|
|
[UnityTest]
|
|
public IEnumerator WriteAsync_RotatesTwoBackupsAtomically() => UniTask.ToCoroutine(async () =>
|
|
{
|
|
var relativePath = "saves/sample.sav";
|
|
|
|
await _provider.WriteAsync(relativePath, DataSerializer.Serialize(new { version = 1 }));
|
|
await _provider.WriteAsync(relativePath, DataSerializer.Serialize(new { version = 2 }));
|
|
await _provider.WriteAsync(relativePath, DataSerializer.Serialize(new { version = 3 }));
|
|
|
|
var primary = DataSerializer.Deserialize<JObject>(await _provider.ReadAsync(relativePath));
|
|
var backup1 = DataSerializer.Deserialize<JObject>(await _provider.ReadAsync(relativePath + ".bak1"));
|
|
var backup2 = DataSerializer.Deserialize<JObject>(await _provider.ReadAsync(relativePath + ".bak2"));
|
|
|
|
Assert.AreEqual(3, primary["version"]?.Value<int>());
|
|
Assert.AreEqual(2, backup1["version"]?.Value<int>());
|
|
Assert.AreEqual(1, backup2["version"]?.Value<int>());
|
|
});
|
|
}
|
|
}
|