using System; using System.Collections; using System.IO; using System.Text; using System.Threading.Tasks; using Cysharp.Threading.Tasks; using NUnit.Framework; using UnityEngine.TestTools; namespace ShrinkDataSaver.Tests { [TestFixture] public class LocalStorageProviderStrategyTests { private string _rootPath; [SetUp] public void SetUp() { _rootPath = Path.Combine(Path.GetTempPath(), $"ShrinkDataSaverStrategyTests_{Guid.NewGuid():N}"); Directory.CreateDirectory(_rootPath); } [TearDown] public void TearDown() { if (Directory.Exists(_rootPath)) { Directory.Delete(_rootPath, true); } } [Test] public void DefaultConstructor_UsesFileReplace() { Assert.AreEqual(LocalStorageCommitStrategy.FileReplace, new LocalStorageProvider(_rootPath).CommitStrategy); } [UnityTest] public IEnumerator RenameOnly_RotatesTwoBackups() => UniTask.ToCoroutine(async () => { var provider = new LocalStorageProvider(_rootPath, LocalStorageCommitStrategy.RenameOnly, TestMove); await WriteAsync(provider, "one"); await WriteAsync(provider, "two"); await WriteAsync(provider, "three"); Assert.AreEqual("three", await ReadAsync(provider, "save.dat")); Assert.AreEqual("two", await ReadAsync(provider, "save.dat.bak1")); Assert.AreEqual("one", await ReadAsync(provider, "save.dat.bak2")); }); [UnityTest] public IEnumerator RenameOnly_RecoversPrimaryFromBak1_WhenPrimaryMissing() => UniTask.ToCoroutine(async () => { var provider = new LocalStorageProvider(_rootPath, LocalStorageCommitStrategy.RenameOnly, TestMove); await WriteAsync(provider, "one"); await WriteAsync(provider, "two"); var primary = Path.Combine(_rootPath, "save.dat"); var primaryBackup = primary + ".bak1"; File.Delete(primaryBackup); File.Move(primary, primaryBackup); Assert.IsTrue(await provider.ExistsAsync("save.dat")); Assert.AreEqual("two", await ReadAsync(provider, "save.dat")); Assert.IsTrue(File.Exists(primary)); Assert.IsFalse(File.Exists(primaryBackup)); }); [UnityTest] public IEnumerator RenameOnly_RecoversPrimaryFromBak2_WhenBak1Missing() => UniTask.ToCoroutine(async () => { var provider = new LocalStorageProvider(_rootPath, LocalStorageCommitStrategy.RenameOnly, TestMove); await WriteAsync(provider, "one"); await WriteAsync(provider, "two"); await WriteAsync(provider, "three"); var primary = Path.Combine(_rootPath, "save.dat"); File.Delete(primary); File.Delete(primary + ".bak1"); Assert.AreEqual("one", await ReadAsync(provider, "save.dat")); }); [UnityTest] public IEnumerator RenameOnly_DeletesStaleTempBeforeWrite() => UniTask.ToCoroutine(async () => { var provider = new LocalStorageProvider(_rootPath, LocalStorageCommitStrategy.RenameOnly, TestMove); await WriteAsync(provider, "one"); var primary = Path.Combine(_rootPath, "save.dat"); File.WriteAllText(primary + ".tmp", "stale", Encoding.UTF8); await WriteAsync(provider, "two"); Assert.IsFalse(File.Exists(primary + ".tmp")); Assert.AreEqual("two", await ReadAsync(provider, "save.dat")); }); [UnityTest] public IEnumerator RenameOnly_PreservesSecondGeneration_WhenBak1Missing() => UniTask.ToCoroutine(async () => { var provider = new LocalStorageProvider(_rootPath, LocalStorageCommitStrategy.RenameOnly, TestMove); await WriteAsync(provider, "one"); await WriteAsync(provider, "two"); await WriteAsync(provider, "three"); var primary = Path.Combine(_rootPath, "save.dat"); File.Delete(primary + ".bak1"); await WriteAsync(provider, "four"); Assert.AreEqual("four", await ReadAsync(provider, "save.dat")); Assert.AreEqual("three", await ReadAsync(provider, "save.dat.bak1")); Assert.AreEqual("one", await ReadAsync(provider, "save.dat.bak2")); }); [UnityTest] public IEnumerator RenameOnly_DoesNotPromoteOrphanTemp() => UniTask.ToCoroutine(async () => { var provider = new LocalStorageProvider(_rootPath, LocalStorageCommitStrategy.RenameOnly, TestMove); var primary = Path.Combine(_rootPath, "save.dat"); File.WriteAllText(primary + ".tmp", "partial", new UTF8Encoding(false)); Assert.IsFalse(await provider.ExistsAsync("save.dat")); Assert.IsFalse(File.Exists(primary)); Assert.IsFalse(File.Exists(primary + ".tmp")); var readFailed = false; try { await ReadAsync(provider, "save.dat"); } catch (FileNotFoundException) { readFailed = true; } Assert.IsTrue(readFailed); }); [UnityTest] public IEnumerator RenameOnly_PrefersBackupOverPartialTemp() => UniTask.ToCoroutine(async () => { var provider = new LocalStorageProvider(_rootPath, LocalStorageCommitStrategy.RenameOnly, TestMove); await WriteAsync(provider, "one"); await WriteAsync(provider, "two"); var primary = Path.Combine(_rootPath, "save.dat"); File.Delete(primary); File.WriteAllText(primary + ".tmp", "partial", new UTF8Encoding(false)); Assert.IsTrue(await provider.ExistsAsync("save.dat")); Assert.AreEqual("one", await ReadAsync(provider, "save.dat")); Assert.IsFalse(File.Exists(primary + ".tmp")); }); [UnityTest] public IEnumerator FileReplace_DoesNotApplyRenameOnlyRecovery() => UniTask.ToCoroutine(async () => { var provider = new LocalStorageProvider(_rootPath); await WriteAsync(provider, "one"); await WriteAsync(provider, "two"); var primary = Path.Combine(_rootPath, "save.dat"); File.Delete(primary); File.WriteAllText(primary + ".tmp", "partial", new UTF8Encoding(false)); Assert.IsFalse(await provider.ExistsAsync("save.dat")); Assert.IsTrue(File.Exists(primary + ".bak1")); Assert.IsTrue(File.Exists(primary + ".tmp")); var readFailed = false; try { await ReadAsync(provider, "save.dat"); } catch (FileNotFoundException) { readFailed = true; } Assert.IsTrue(readFailed); }); [UnityTest] public IEnumerator DeleteAsync_RemovesBackupsAndTemp() => UniTask.ToCoroutine(async () => { var provider = new LocalStorageProvider(_rootPath, LocalStorageCommitStrategy.RenameOnly, TestMove); await WriteAsync(provider, "one"); await WriteAsync(provider, "two"); var primary = Path.Combine(_rootPath, "save.dat"); File.WriteAllText(primary + ".tmp", "stale", Encoding.UTF8); await provider.DeleteAsync("save.dat"); Assert.IsFalse(File.Exists(primary)); Assert.IsFalse(File.Exists(primary + ".bak1")); Assert.IsFalse(File.Exists(primary + ".bak2")); Assert.IsFalse(File.Exists(primary + ".tmp")); }); [UnityTest] public IEnumerator PlatformInitialize_UsesInjectedStorage() => UniTask.ToCoroutine(async () => { var storage = new MockStorageProvider(); ShrinkSettings.ResetForTesting(); ShrinkDataSaverPlatform.Initialize(storage, "runtime-root"); ShrinkSettings.Set("injected", 7); await ShrinkSettings.SaveAsync(); Assert.IsTrue(await storage.ExistsAsync("runtime-root/settings.json")); var bytes = await storage.ReadAsync("runtime-root/settings.json"); StringAssert.Contains("\"injected\"", Encoding.UTF8.GetString(bytes)); }); // Tests the shared rotation contract; Godot integration tests exercise actual overwrite rename. private static void TestMove(string source, string destination) { if (File.Exists(destination)) File.Delete(destination); File.Move(source, destination); } [Test] public void RenameOnly_RequiresOverwriteCapability() => Assert.Throws(() => new LocalStorageProvider(_rootPath, LocalStorageCommitStrategy.RenameOnly)); private static async UniTask WriteAsync(IStorageProvider provider, string value) { await provider.WriteAsync("save.dat", Encoding.UTF8.GetBytes(value)); } private static async UniTask ReadAsync(IStorageProvider provider, string path) { var bytes = await provider.ReadAsync(path); return Encoding.UTF8.GetString(bytes); } } }