From 7dfd87e868dc14036d1280abcccf165e0f70a8de Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Thu, 16 Jul 2026 18:40:14 +0800 Subject: [PATCH] =?UTF-8?q?test(save):=20=E6=B7=BB=E5=8A=A0=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E5=AD=98=E6=A1=A3=E6=B8=85=E5=8D=95=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DevSavePromoteWindow.cs | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/Assets/Editor/SaveSystemValidation/DevSavePromoteWindow.cs b/Assets/Editor/SaveSystemValidation/DevSavePromoteWindow.cs index 4c7e5cd7d..023ef6b65 100644 --- a/Assets/Editor/SaveSystemValidation/DevSavePromoteWindow.cs +++ b/Assets/Editor/SaveSystemValidation/DevSavePromoteWindow.cs @@ -7,6 +7,7 @@ using System.Text; using AibisDream.SaveSystem; using AibisDream.Utility; using Newtonsoft.Json; +using NUnit.Framework; using UnityEditor; using UnityEditor.AddressableAssets; using UnityEditor.AddressableAssets.Settings; @@ -374,5 +375,139 @@ namespace AibisDream.EditorTools } } } + + [TestFixture] + public sealed class DevSaveCatalogTests + { + private string root; + + [SetUp] + public void SetUp() + { + root = Path.Combine(Path.GetTempPath(), $"aibis-dev-save-{Guid.NewGuid():N}"); + Directory.CreateDirectory(root); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(root)) Directory.Delete(root, true); + } + + [Test] + public void CatalogUsesExplicitOrderAndKeepsInvalidEntriesVisible() + { + WriteCatalog(new DevSaveCatalogEntryDto { order = 20, label = "late", path = "S/late", anchorNode = "Late" }, + new DevSaveCatalogEntryDto { order = 10, label = "early", path = "S/early", anchorNode = "Early" }); + + var section = DevSaveCatalog.Load(root).Single(); + + Assert.That(section.Entries.Select(entry => entry.Label), Is.EqualTo(new[] { "early", "late" })); + Assert.That(section.Entries.All(entry => !entry.IsValid), Is.True); + Assert.That(section.Entries.All(entry => entry.Error.Contains("缺失")), Is.True); + } + + [Test] + public void CatalogRejectsPathTraversal() + { + WriteCatalog(new DevSaveCatalogEntryDto { order = 1, label = "unsafe", path = "../outside", anchorNode = "Start" }); + + var entry = DevSaveCatalog.Load(root).Single().Entries.Single(); + + Assert.That(entry.IsValid, Is.False); + Assert.That(entry.Error, Does.Contain("越过")); + } + + [Test] + public void CatalogReportsCorruptSchemaAndAnchorMismatch() + { + WriteSnapshot("S/corrupt", "{"); + WriteSnapshot("S/schema", JsonConvert.SerializeObject(BuildSnapshot("Node", schema: 99))); + WriteSnapshot("S/node", JsonConvert.SerializeObject(BuildSnapshot("Actual"))); + WriteCatalog( + new DevSaveCatalogEntryDto { order = 1, label = "corrupt", path = "S/corrupt", anchorNode = "Node" }, + new DevSaveCatalogEntryDto { order = 2, label = "schema", path = "S/schema", anchorNode = "Node" }, + new DevSaveCatalogEntryDto { order = 3, label = "node", path = "S/node", anchorNode = "Expected" }); + + var entries = DevSaveCatalog.Load(root).Single().Entries; + + Assert.That(entries[0].Error, Does.Contain("JSON")); + Assert.That(entries[1].Error, Does.Contain("版本")); + Assert.That(entries[2].Error, Does.Contain("节点不匹配")); + } + + [Test] + public void CommittedCatalogContainsOnlyValidUniqueEntries() + { + var sections = DevSaveCatalog.Load(ConstRef.TestSaveFilePath); + Assert.That(DevSaveCatalog.LastError, Is.Null); + Assert.That(sections.Count, Is.GreaterThan(0)); + foreach (var section in sections) + { + Assert.That(section.Entries, Is.Not.Empty, section.Id); + Assert.That(section.Entries.All(entry => entry.IsValid), Is.True, + string.Join("\n", section.Entries.Where(entry => !entry.IsValid).Select(entry => $"{entry.Label}: {entry.Error}"))); + Assert.That(section.Entries.Select(entry => entry.Order).Distinct().Count(), Is.EqualTo(section.Entries.Count)); + Assert.That(section.Entries.Select(entry => entry.AnchorNode).Distinct().Count(), Is.EqualTo(section.Entries.Count)); + } + } + + [Test] + public void StrictRestoreContextTracksPhaseWarningsAndErrors() + { + var context = new SnapshotRestoreContext(new SaveSnapshot(), strictMode: true); + context.SetPhase("Provider restore"); + context.Warn("optional state missing"); + context.Error("required state missing"); + + Assert.That(context.StrictMode, Is.True); + Assert.That(context.CurrentPhase, Is.EqualTo("Provider restore")); + Assert.That(context.Warnings, Is.EqualTo(new[] { "optional state missing" })); + Assert.That(context.Errors, Is.EqualTo(new[] { "required state missing" })); + Assert.That(context.HasErrors, Is.True); + } + + private void WriteCatalog(params DevSaveCatalogEntryDto[] entries) + { + var catalog = new DevSaveCatalogDto + { + sections = new List + { + new() + { + id = "S", + title = "Section", + expectedSceneName = "Scene/Test", + expectedSceneSoName = "TestSO", + expectedYarnProjectId = "TestYarn", + entries = entries.ToList() + } + } + }; + File.WriteAllText(Path.Combine(root, "catalog.json"), JsonConvert.SerializeObject(catalog)); + } + + private void WriteSnapshot(string relativeDirectory, string json) + { + var directory = Path.Combine(root, relativeDirectory.Replace('/', Path.DirectorySeparatorChar)); + Directory.CreateDirectory(directory); + File.WriteAllText(Path.Combine(directory, "snapshot.json"), json); + } + + private static SaveSnapshot BuildSnapshot(string node, int schema = SaveSnapshotSchema.CurrentVersion) + { + return new SaveSnapshot + { + schemaVersion = schema, + scene = new SceneSnapshotDto { sceneName = "Scene/Test" }, + anchor = new AnchorSnapshot + { + sceneSoName = "TestSO", + yarnProjectId = "TestYarn", + nodeName = node + } + }; + } + } } #endif