Files
ShrinkDataSaver/Tests/ShrinkSaveTests.cs
T
cneicy 739fe328e7
Publish UPM package / publish (push) Failing after 1s
chore: initialize standalone UPM package
2026-08-26 02:50:03 +08:00

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>());
});
}
}