diff --git a/Runtime/ISaveable.cs b/Runtime/ISaveable.cs index 35d4e8c..31773fc 100644 --- a/Runtime/ISaveable.cs +++ b/Runtime/ISaveable.cs @@ -12,12 +12,18 @@ public interface ISaveable /// If you choose to use a Guid, it is recommended that it is backed by a /// serialized byte array that does not change. /// + /// + /// This is invoked off Unity's main thread, cannot use thread unsafe APIs. + /// public string Key { get; } /// /// This is the file name where this object's data will be saved. /// It is recommended to use a static class to store file paths as strings to avoid typos. /// + /// + /// This is invoked on Unity's main thread, safe to use thread unsafe APIs. + /// public string Filename { get; } /// @@ -25,6 +31,9 @@ public interface ISaveable /// Typically this is a struct defined by the ISaveable implementing class. /// The contents of the struct could be created at the time of saving, or cached in a variable. /// + /// + /// This is invoked off Unity's main thread, cannot use thread unsafe APIs. + /// object CaptureState(); /// @@ -32,6 +41,9 @@ public interface ISaveable /// This will be called any time the game is loaded, so you may want to consider /// also using this method to initialize any fields that are not saved (i.e. "resetting the object"). /// + /// + /// This is invoked on Unity's main thread, safe to use thread unsafe APIs. + /// void RestoreState(object state); } } diff --git a/Runtime/SaveManager.cs b/Runtime/SaveManager.cs index 9823028..7b71402 100644 --- a/Runtime/SaveManager.cs +++ b/Runtime/SaveManager.cs @@ -52,9 +52,20 @@ public FileOperation(FileOperationType operationType, string[] filenames) } static FileHandler m_fileHandler; + /// + /// Saveables which have registered themselves inside the manager. + /// static Dictionary m_saveables = new(); + /// + /// Temporary working memory used during a operation. Stores + /// data which was loaded from a save file and will be restored to ISaveables after the load operation completes. + /// static List m_loadedSaveables = new(); static Queue m_fileOperationQueue = new(); + /// + /// A set of all files associated with currently registered ISaveables. + /// Currently unused. + /// static HashSet m_files = new(); static bool m_isInitialized; diff --git a/Tests.meta b/Tests.meta new file mode 100644 index 0000000..88deba7 --- /dev/null +++ b/Tests.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3220b088ee9d4e60826b78a6544fe1d4 +timeCreated: 1725243685 \ No newline at end of file diff --git a/Tests/Runtime.meta b/Tests/Runtime.meta new file mode 100644 index 0000000..09c2645 --- /dev/null +++ b/Tests/Runtime.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3e8afb4fbe54475f964f2abea3160a68 +timeCreated: 1725243792 \ No newline at end of file diff --git a/Tests/Runtime/BUCK.SaveAsync.Tests.asmdef b/Tests/Runtime/BUCK.SaveAsync.Tests.asmdef new file mode 100644 index 0000000..ab052b9 --- /dev/null +++ b/Tests/Runtime/BUCK.SaveAsync.Tests.asmdef @@ -0,0 +1,22 @@ +{ + "name": "BUCK.SaveAsync.Tests", + "rootNamespace": "Buck.SaveAsync.Tests", + "references": [ + "GUID:27619889b8ba8c24980f49ee34dbb44a", + "GUID:0acc523941302664db1f4e527237feb3", + "GUID:ad4bea86cb6093347bcf3482d1635cc9" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/Runtime/BUCK.SaveAsync.Tests.asmdef.meta b/Tests/Runtime/BUCK.SaveAsync.Tests.asmdef.meta new file mode 100644 index 0000000..c01085b --- /dev/null +++ b/Tests/Runtime/BUCK.SaveAsync.Tests.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 06d4c76cc3914fce9036eb37a7ec33ae +timeCreated: 1725243828 \ No newline at end of file diff --git a/Tests/Runtime/TestConstants.cs b/Tests/Runtime/TestConstants.cs new file mode 100644 index 0000000..1f4d762 --- /dev/null +++ b/Tests/Runtime/TestConstants.cs @@ -0,0 +1,14 @@ +namespace Buck.SaveAsync.Tests +{ + public static class TestConstants + { + /// + /// Asserted against when the namespace is included in json serialization output + /// + public static string Namespace => "Buck.SaveAsync.Tests"; + /// + /// Asserted against when the assembly is included in json serialization output + /// + public static string Assembly => "BUCK.SaveAsync.Tests"; + } +} \ No newline at end of file diff --git a/Tests/Runtime/TestConstants.cs.meta b/Tests/Runtime/TestConstants.cs.meta new file mode 100644 index 0000000..590358a --- /dev/null +++ b/Tests/Runtime/TestConstants.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 13c2e224a35d46acad45b370dcd4b1c0 +timeCreated: 1725250898 \ No newline at end of file diff --git a/Tests/Runtime/TestRoundTripSaveLoad.cs b/Tests/Runtime/TestRoundTripSaveLoad.cs new file mode 100644 index 0000000..8e40a9f --- /dev/null +++ b/Tests/Runtime/TestRoundTripSaveLoad.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Buck.SaveAsync.Tests +{ + /// + /// These tests verify round-trip save and load by saving a state, changing the state, + /// and then loading the previously saved state. + /// + public class TestRoundTripSaveLoad : TestCaseBase + { + [UnityTest] + public IEnumerator Test_RoundTrip_String() + { + async Awaitable Impl() + { + var expected = "Hello, World!"; + var actual = await GetRoundTrip(expected, "Goodbye, World!"); + + Assert.AreEqual(expected, actual); + } + + return Impl(); + } + + [UnityTest] + public IEnumerator Test_RoundTrip_String_WithDelay() + { + async Awaitable Impl() + { + var expected = "Hello, World!"; + var actual = await GetRoundTrip(expected, "Goodbye, World!", TimeSpan.FromSeconds(0.3f)); + Assert.AreEqual(expected, actual); + } + + return Impl(); + } + + class SaveObjectWithNestedVector3 + { + public Vector3 NestedVector3 { get; set; } + } + + [UnityTest] + public IEnumerator Test_RoundTrip_Vector3Nested() + { + async Awaitable Impl() + { + var expected = new Vector3(1, 2.3f, 10000.2f); + var actual = await GetRoundTrip(new SaveObjectWithNestedVector3 + { + NestedVector3 = expected + }, + new SaveObjectWithNestedVector3 + { + NestedVector3 = Vector3.zero + }); + Assert.AreEqual(0, (expected - actual.NestedVector3).magnitude, 0.0001f); + } + + return Impl(); + } + + [UnityTest] + public IEnumerator Test_RoundTrip_Vector3Raw() + { + async Awaitable Impl() + { + var expected = new Vector3(1, 2.3f, 10000.2f); + var actual = await GetRoundTrip(expected, Vector3.zero); + Assert.AreEqual(0, (expected - actual).magnitude, 0.0001f); + } + + return Impl(); + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/TestRoundTripSaveLoad.cs.meta b/Tests/Runtime/TestRoundTripSaveLoad.cs.meta new file mode 100644 index 0000000..1972a33 --- /dev/null +++ b/Tests/Runtime/TestRoundTripSaveLoad.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a41fed8bd4e443518725d337830d0661 +timeCreated: 1725243954 \ No newline at end of file diff --git a/Tests/Runtime/TestSaveFileFormat.cs b/Tests/Runtime/TestSaveFileFormat.cs new file mode 100644 index 0000000..6b33a22 --- /dev/null +++ b/Tests/Runtime/TestSaveFileFormat.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Buck.SaveAsync.Tests +{ + internal class TestSaveObject + { + public int IntValue { get; set; } + public string StringValue { get; set; } + } + + /// + /// These tests verify the json format of save files generated by the save system. Useful to detect when a change + /// is introduced which may break existing saves. + /// + public class TestSaveFileFormat : TestCaseBase + { + [UnityTest] + public IEnumerator Test_SaveFormat_NestedDictionary() + { + async Awaitable Impl() + { + // Arrange + var nestedObject = new Dictionary + { + { "key1", "value1" }, + { "key2", 2 }, + { + "key3", new Dictionary + { + { "key4", "value4" }, + { "key5", 5 } + } + } + }; + + // Act + var key = Guid.NewGuid().ToString(); + var serializedFile = await GetSerializedFileForObject(key, nestedObject); + + // Assert + var expected = $@" +[ + {{ + ""Key"": ""{key}"", + ""Data"": {{ + ""$type"": ""System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib"", + ""key1"": ""value1"", + ""key2"": 2, + ""key3"": {{ + ""$type"": ""System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Object, mscorlib]], mscorlib"", + ""key4"": ""value4"", + ""key5"": 5 + }} + }} + }} +] +"; + MultilineDiffUtils.AssertMultilineStringEqual(expected,serializedFile); + } + + return Impl(); + } + + [UnityTest] + public IEnumerator Test_SaveFormat_BasicObject() + { + async Awaitable Impl() + { + // Arrange + var nestedObject = new TestSaveObject + { + IntValue = 1337, + StringValue = "Goodbye, World!" + }; + + // Act + var key = Guid.NewGuid().ToString(); + var serializedFile = await GetSerializedFileForObject(key, nestedObject); + + // Assert + var expected = $@" +[ + {{ + ""Key"": ""{key}"", + ""Data"": {{ + ""$type"": ""{TestConstants.Namespace}.TestSaveObject, {TestConstants.Assembly}"", + ""IntValue"": 1337, + ""StringValue"": ""Goodbye, World!"" + }} + }} +] +"; + MultilineDiffUtils.AssertMultilineStringEqual(expected,serializedFile); + } + + return Impl(); + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/TestSaveFileFormat.cs.meta b/Tests/Runtime/TestSaveFileFormat.cs.meta new file mode 100644 index 0000000..0c18fc0 --- /dev/null +++ b/Tests/Runtime/TestSaveFileFormat.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6596da966b6e4250a735feb06943a76d +timeCreated: 1725247266 \ No newline at end of file diff --git a/Tests/Runtime/TestTools.meta b/Tests/Runtime/TestTools.meta new file mode 100644 index 0000000..f1d1119 --- /dev/null +++ b/Tests/Runtime/TestTools.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3377ad339e18464ba08c76a0ef386df1 +timeCreated: 1725249891 \ No newline at end of file diff --git a/Tests/Runtime/TestTools/InMemoryFileHandler.cs b/Tests/Runtime/TestTools/InMemoryFileHandler.cs new file mode 100644 index 0000000..e49264f --- /dev/null +++ b/Tests/Runtime/TestTools/InMemoryFileHandler.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Buck.SaveAsync.Tests +{ + /// + /// A FileHandler used for testing which does not write anything to disk, but instead + /// stores data in memory. Can be configured to simulate slow operations. + /// + public class InMemoryFileHandler : FileHandler + { + public TimeSpan AllOperationDelay { get; set; } = TimeSpan.Zero; + + readonly Dictionary m_files = new(); + + protected override string GetPath(string pathOrFilename) => pathOrFilename; + + public override async Task Exists(string pathOrFilename, CancellationToken cancellationToken) + { + await Task.Delay(AllOperationDelay, cancellationToken); + return m_files.ContainsKey(pathOrFilename); + } + + public override async Task WriteFile(string pathOrFilename, string content, CancellationToken cancellationToken) + { + await Task.Delay(AllOperationDelay, cancellationToken); + m_files[pathOrFilename] = content; + } + + public override async Task ReadFile(string pathOrFilename, CancellationToken cancellationToken) + { + await Task.Delay(AllOperationDelay, cancellationToken); + return m_files[pathOrFilename] ?? ""; + } + + public override async Task Erase(string pathOrFilename, CancellationToken cancellationToken) + { + await Task.Delay(AllOperationDelay, cancellationToken); + m_files[pathOrFilename] = ""; + } + + public override void Delete(string pathOrFilename) + { + // Delete is sync, no delay + m_files.Remove(pathOrFilename); + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/TestTools/InMemoryFileHandler.cs.meta b/Tests/Runtime/TestTools/InMemoryFileHandler.cs.meta new file mode 100644 index 0000000..6103d4d --- /dev/null +++ b/Tests/Runtime/TestTools/InMemoryFileHandler.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 11bcd2d87ab941bc87862dff5eb716d5 +timeCreated: 1725244017 \ No newline at end of file diff --git a/Tests/Runtime/TestTools/MultilineDiffUtils.cs b/Tests/Runtime/TestTools/MultilineDiffUtils.cs new file mode 100644 index 0000000..519e394 --- /dev/null +++ b/Tests/Runtime/TestTools/MultilineDiffUtils.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; + +namespace Buck.SaveAsync.Tests +{ + /// + /// Helpers which assert equality of multiline strings in a readable way + /// + public class MultilineDiffUtils + { + public static void AssertMultilineStringEqual(string expected, string actual) + { + expected = expected.Trim(); + actual = actual.Trim(); + if (expected == actual) return; + Assert.Fail(StringEqualErrorMessage(expected, actual)); + } + + public static string StringEqualErrorMessage(string expected, string actual) + { + var errorMessage = new StringBuilder(); + errorMessage.AppendLine($"#### Expected ####\n{expected}\n#### Actual ####\n{actual}"); + var expectedStrLines = expected.Split('\n'); + var actualStrLines = actual.Split('\n'); + if (expectedStrLines.Length == actualStrLines.Length) + { + foreach (var (index, expectedLine, actualLine) in DiffLines(expectedStrLines, actualStrLines)) + { + var addlMessage = $"##{index}##\n- {expectedLine}\n+ {actualLine}"; + errorMessage.AppendLine(addlMessage); + } + } + return errorMessage.ToString(); + } + + static IEnumerable<(int index, string expected, string actual)> DiffLines(string[] expected, string[] actual) + { + for (var i = 0; i < expected.Length; i++) + { + var expectedLine = expected[i].Trim(); + var actualLine = actual[i].Trim(); + if(expectedLine == actualLine) continue; + yield return (i, expectedLine, actualLine); + } + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/TestTools/MultilineDiffUtils.cs.meta b/Tests/Runtime/TestTools/MultilineDiffUtils.cs.meta new file mode 100644 index 0000000..7e4d09f --- /dev/null +++ b/Tests/Runtime/TestTools/MultilineDiffUtils.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a7adc162acf04c88b8df54e713265688 +timeCreated: 1725247693 \ No newline at end of file diff --git a/Tests/Runtime/TestTools/SaveManagerExtensions.cs b/Tests/Runtime/TestTools/SaveManagerExtensions.cs new file mode 100644 index 0000000..c2c6d2f --- /dev/null +++ b/Tests/Runtime/TestTools/SaveManagerExtensions.cs @@ -0,0 +1,31 @@ +using System; + +namespace Buck.SaveAsync.Tests +{ + /// + /// These are extensions to configure the SaveManager in potentially nonstandard ways, required in order to test it. + /// Could be placed inside SaveManager if it is appropriate to expose a public API for these functions, rather than a testing-only API. + /// + public static class SaveManagerExtensions + { + /// + /// Set the static file handler used by the SaveManager singleton. + /// + /// + public static void SetCustomFileHandler(FileHandler newFileHander) + { + if (!newFileHander) + { + throw new ArgumentNullException(nameof(newFileHander)); + } + + var fileHandlerField = typeof(SaveManager) + .GetField("m_fileHandler", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + if (fileHandlerField == null) + { + throw new InvalidOperationException("Could not find static private field 'm_fileHandler' on SaveManager."); + } + fileHandlerField.SetValue(null, newFileHander); + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/TestTools/SaveManagerExtensions.cs.meta b/Tests/Runtime/TestTools/SaveManagerExtensions.cs.meta new file mode 100644 index 0000000..6fea56d --- /dev/null +++ b/Tests/Runtime/TestTools/SaveManagerExtensions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a4b127ef92c3444e8406b129190e86e8 +timeCreated: 1725245422 \ No newline at end of file diff --git a/Tests/Runtime/TestTools/TestCaseBase.cs b/Tests/Runtime/TestTools/TestCaseBase.cs new file mode 100644 index 0000000..bbbbbe1 --- /dev/null +++ b/Tests/Runtime/TestTools/TestCaseBase.cs @@ -0,0 +1,91 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using NUnit.Framework; +using UnityEngine; + +namespace Buck.SaveAsync.Tests +{ + /// + /// A set of base methods used by most test cases. Overridable to provide different save manager setup methods, + /// useful when we want to configure JsonConvert's default settings for example. + /// + public class TestCaseBase + { + protected virtual void SetupSaveManager(FileHandler withFileHandler) + { + SaveManagerExtensions.SetCustomFileHandler(withFileHandler); + // ensure that the default settings are not overriden, for test consistency. + JsonConvert.DefaultSettings = null; + } + + protected FileHandler CreateFileHandler(TimeSpan? withEmulatedDelay = null) + { + var fileHandler = ScriptableObject.CreateInstance(); + fileHandler.AllOperationDelay = withEmulatedDelay ?? TimeSpan.Zero; + return fileHandler; + } + + protected TestSaveableEntity CreateSaveableEntity(string key, string filename = "test.dat") + { + var saveableEntity = new GameObject(); + var saveable = saveableEntity.AddComponent(); + saveable.Key = key; + saveable.Filename = filename; + saveable.RegisterSelf(); + return saveable; + } + + /// + /// Creates a saveable object, saves it to an emulated file handler, and returns + /// the serialized string from the emulated file handler. + /// + /// The key of the saveable object + /// The data to place inside the saveable object + /// The string value which is saved to file when the temporary saveable object is saved + protected async Task GetSerializedFileForObject(string key, object savedObject) + { + var fileName = Guid.NewGuid() + ".dat"; + + var fileHandler = CreateFileHandler(); + SetupSaveManager(fileHandler); + + var saveable = CreateSaveableEntity(key, fileName); + saveable.CurrentState = savedObject; + await SaveManager.Save(fileName); + + return await fileHandler.ReadFile(fileName, CancellationToken.None); + } + + /// + /// Tests round-trip serialization of a given value. + /// is round-tripped through the save system backed by a temporary file handler. + /// + /// The value to be round-tripped + /// A value used to clear the saveable's internal state, to ensure the round-trip isn't a result of a cached value. + /// Optional artificial delay applied to the file handler + /// + /// The value in the saveable after it has been Loaded + protected async Task GetRoundTrip(T initial, T resetTo, TimeSpan? fileHandlerDelay = null) + { + // Arrange + var seed = Guid.NewGuid().ToString(); + + var fileHandler = CreateFileHandler(fileHandlerDelay); + SetupSaveManager(fileHandler); + var saveable = CreateSaveableEntity("saveable_" + seed, "test.dat"); + + saveable.CurrentState = initial; + await SaveManager.Save("test.dat"); + + // Act + saveable.CurrentState = resetTo; + await SaveManager.Load("test.dat"); + + // Assert + Assert.IsAssignableFrom(typeof(T), saveable.CurrentState); + return (T)saveable.CurrentState; + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/TestTools/TestCaseBase.cs.meta b/Tests/Runtime/TestTools/TestCaseBase.cs.meta new file mode 100644 index 0000000..684386f --- /dev/null +++ b/Tests/Runtime/TestTools/TestCaseBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b938ab02942849599696dc5ffaf9e899 +timeCreated: 1725249929 \ No newline at end of file diff --git a/Tests/Runtime/TestTools/TestSaveableEntity.cs b/Tests/Runtime/TestTools/TestSaveableEntity.cs new file mode 100644 index 0000000..ef49eea --- /dev/null +++ b/Tests/Runtime/TestTools/TestSaveableEntity.cs @@ -0,0 +1,22 @@ +using UnityEngine; + +namespace Buck.SaveAsync.Tests +{ + /// + /// A test MonoBehavior that implements saveable. Its internals are exposed since it is piloted + /// from test cases. + /// + public class TestSaveableEntity : MonoBehaviour, ISaveable + { + public string Key { get; set; } = nameof(TestSaveableEntity); + public string Filename { get; set; } + public object CurrentState { get; set; } + public object CaptureState() => CurrentState; + public void RestoreState(object state) => this.CurrentState = state; + + public void RegisterSelf() + { + SaveManager.RegisterSaveable(this); + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/TestTools/TestSaveableEntity.cs.meta b/Tests/Runtime/TestTools/TestSaveableEntity.cs.meta new file mode 100644 index 0000000..4ed4ed0 --- /dev/null +++ b/Tests/Runtime/TestTools/TestSaveableEntity.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 41bd6787a3a447d394ae94b49cc7d3b7 +timeCreated: 1725244973 \ No newline at end of file diff --git a/Tests/Runtime/UnityConverters.meta b/Tests/Runtime/UnityConverters.meta new file mode 100644 index 0000000..07c5c83 --- /dev/null +++ b/Tests/Runtime/UnityConverters.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ed79852fba21f6b4a964e75637ceb74a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Runtime/UnityConverters/BUCK.SaveAsync.UnityConverters.Tests.asmdef b/Tests/Runtime/UnityConverters/BUCK.SaveAsync.UnityConverters.Tests.asmdef new file mode 100644 index 0000000..ecf110a --- /dev/null +++ b/Tests/Runtime/UnityConverters/BUCK.SaveAsync.UnityConverters.Tests.asmdef @@ -0,0 +1,31 @@ +{ + "name": "BUCK.SaveAsync.Tests.UnityConverters", + "rootNamespace": "Buck.SaveAsync.Tests", + "references": [ + "GUID:27619889b8ba8c24980f49ee34dbb44a", + "GUID:0acc523941302664db1f4e527237feb3", + "GUID:ad4bea86cb6093347bcf3482d1635cc9", + "GUID:06d4c76cc3914fce9036eb37a7ec33ae", + "GUID:c55d28459d9c4444ebb4788be912e1f1" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS", + "HAVE_JSON_FOR_UNITY_CONVERTERS" + ], + "versionDefines": [ + { + "name": "jillejr.newtonsoft.json-for-unity.converters", + "expression": "", + "define": "HAVE_JSON_FOR_UNITY_CONVERTERS" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Tests/Runtime/UnityConverters/BUCK.SaveAsync.UnityConverters.Tests.asmdef.meta b/Tests/Runtime/UnityConverters/BUCK.SaveAsync.UnityConverters.Tests.asmdef.meta new file mode 100644 index 0000000..1a92595 --- /dev/null +++ b/Tests/Runtime/UnityConverters/BUCK.SaveAsync.UnityConverters.Tests.asmdef.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 82e0ee47d15b6954ab150e78908f6c69 +timeCreated: 1725243828 diff --git a/Tests/Runtime/UnityConverters/TestRoundTripSaveLoadWithUnityConverters.cs b/Tests/Runtime/UnityConverters/TestRoundTripSaveLoadWithUnityConverters.cs new file mode 100644 index 0000000..b74cb27 --- /dev/null +++ b/Tests/Runtime/UnityConverters/TestRoundTripSaveLoadWithUnityConverters.cs @@ -0,0 +1,55 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Buck.SaveAsync.Tests +{ + /// + /// These tests verify round-trip save and load by saving a state, changing the state, + /// and then loading the previously saved state. + /// + /// + /// specifically meant for testing integration with Json-for-Unity converters, + /// see https://github.com/applejag/Newtonsoft.Json-for-Unity.Converters + /// + public class TestRoundTripSaveLoadWithUnityConverters : UnityConverterTestCaseBase + { + class SaveObjectWithNestedVector3 + { + public Vector3 NestedVector3 { get; set; } + } + + [UnityTest] + public IEnumerator TestUnityConverter_RoundTrip_Vector3Nested() { + async Awaitable Impl() + { + var expected = new Vector3(1, 2.3f, 10000.2f); + var actual = await GetRoundTrip(new SaveObjectWithNestedVector3 + { + NestedVector3 = expected + }, + new SaveObjectWithNestedVector3 + { + NestedVector3 = Vector3.zero + }); + Assert.AreEqual(0, (expected - actual.NestedVector3).magnitude, 0.0001f); + } + + return Impl(); + } + + [UnityTest] + public IEnumerator TestUnityConverter_RoundTrip_Vector3Raw() + { + async Awaitable Impl() + { + var expected = new Vector3(1, 2.3f, 10000.2f); + var actual = await GetRoundTrip(expected, Vector3.zero); + Assert.AreEqual(0, (expected - actual).magnitude, 0.0001f); + } + + return Impl(); + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/UnityConverters/TestRoundTripSaveLoadWithUnityConverters.cs.meta b/Tests/Runtime/UnityConverters/TestRoundTripSaveLoadWithUnityConverters.cs.meta new file mode 100644 index 0000000..a9ce314 --- /dev/null +++ b/Tests/Runtime/UnityConverters/TestRoundTripSaveLoadWithUnityConverters.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 73732682696c4e2ab8c5cbcb1a8455ae +timeCreated: 1725288339 \ No newline at end of file diff --git a/Tests/Runtime/UnityConverters/TestSaveFileFormatWithUnityConverters.cs b/Tests/Runtime/UnityConverters/TestSaveFileFormatWithUnityConverters.cs new file mode 100644 index 0000000..1b78a95 --- /dev/null +++ b/Tests/Runtime/UnityConverters/TestSaveFileFormatWithUnityConverters.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Buck.SaveAsync.Tests +{ + internal class TestUnitySaveObject + { + public Vector3 Vector3Value { get; set; } + public Quaternion QuaternionValue { get; set; } + public Color ColorValue { get; set; } + public AnimationCurve AnimationCurveValue { get; set; } + } + + /// + /// These tests verify the json format of save files generated by the save system. Useful to detect when a change + /// is introduced which may break existing saves. + /// + /// + /// specifically meant for testing integration with Json-for-Unity converters, + /// see https://github.com/applejag/Newtonsoft.Json-for-Unity.Converters + /// + public class TestSaveFileFormatWithUnityConverters : UnityConverterTestCaseBase + { + [UnityTest] + public IEnumerator TestUnityConverter_SaveFormat_UnityPrimitiveObjects() { + async Awaitable Impl() { + // Arrange + var nestedObject = new TestUnitySaveObject + { + Vector3Value = new Vector3(1, 2, 3.5f), + QuaternionValue = new Quaternion(0.1f, 0.2f, 0.3f, 0.4f), + ColorValue = new Color(0.1f, 0.2f, 0.3f, 0.4f), + AnimationCurveValue = AnimationCurve.EaseInOut(0, 0, 1, 1) + }; + // Act + var key = Guid.NewGuid().ToString(); + var serializedFile = await GetSerializedFileForObject(key, nestedObject); + + // Assert + var expected = $@" +[ + {{ + ""Key"": ""{key}"", + ""Data"": {{ + ""$type"": ""{TestConstants.Namespace}.TestUnitySaveObject, {TestConstants.Assembly}.UnityConverters"", + ""Vector3Value"": {{ + ""x"": 1.0, + ""y"": 2.0, + ""z"": 3.5 + }}, + ""QuaternionValue"": {{ + ""x"": 0.1, + ""y"": 0.2, + ""z"": 0.3, + ""w"": 0.4 + }}, + ""ColorValue"": {{ + ""r"": 0.1, + ""g"": 0.2, + ""b"": 0.3, + ""a"": 0.4 + }}, + ""AnimationCurveValue"": {{ + ""keys"": [ + {{ + ""time"": 0.0, + ""value"": 0.0, + ""inTangent"": 0.0, + ""outTangent"": 0.0, + ""inWeight"": 0.0, + ""outWeight"": 0.0, + ""weightedMode"": ""None"", + ""tangentMode"": 0 + }}, + {{ + ""time"": 1.0, + ""value"": 1.0, + ""inTangent"": 0.0, + ""outTangent"": 0.0, + ""inWeight"": 0.0, + ""outWeight"": 0.0, + ""weightedMode"": ""None"", + ""tangentMode"": 0 + }} + ], + ""length"": 2, + ""preWrapMode"": ""ClampForever"", + ""postWrapMode"": ""ClampForever"" + }} + }} + }} +] +"; + MultilineDiffUtils.AssertMultilineStringEqual(expected, serializedFile); + } + + return Impl(); + } + + [UnityTest] + public IEnumerator TestUnityConverter_SaveFormat_Vector3Raw() + { + async Awaitable Impl() + { + // Arrange + var nestedObject = new Vector3(1, 2, 3.5f); + + // Act + var key = Guid.NewGuid().ToString(); + var serializedFile = await GetSerializedFileForObject(key, nestedObject); + + // Assert + var expected = $@" +[ + {{ + ""Key"": ""{key}"", + ""Data"": {{ + ""x"": 1.0, + ""y"": 2.0, + ""z"": 3.5 + }} + }} +] +"; + MultilineDiffUtils.AssertMultilineStringEqual(expected,serializedFile); + } + + return Impl(); + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/UnityConverters/TestSaveFileFormatWithUnityConverters.cs.meta b/Tests/Runtime/UnityConverters/TestSaveFileFormatWithUnityConverters.cs.meta new file mode 100644 index 0000000..5c04108 --- /dev/null +++ b/Tests/Runtime/UnityConverters/TestSaveFileFormatWithUnityConverters.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 79383e7b47f44a7fb8cca613aec3dd6c +timeCreated: 1725250709 \ No newline at end of file diff --git a/Tests/Runtime/UnityConverters/UnityConverterTestCaseBase.cs b/Tests/Runtime/UnityConverters/UnityConverterTestCaseBase.cs new file mode 100644 index 0000000..654b603 --- /dev/null +++ b/Tests/Runtime/UnityConverters/UnityConverterTestCaseBase.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.UnityConverters; + +namespace Buck.SaveAsync.Tests +{ + public class UnityConverterTestCaseBase : TestCaseBase + { + protected override void SetupSaveManager(FileHandler withFileHandler) + { + base.SetupSaveManager(withFileHandler); + // ensure we are using the default Json-for-Unity converters + var jsonSettings = UnityConverterInitializer.defaultUnityConvertersSettings; + JsonConvert.DefaultSettings = () => jsonSettings; + } + } +} \ No newline at end of file diff --git a/Tests/Runtime/UnityConverters/UnityConverterTestCaseBase.cs.meta b/Tests/Runtime/UnityConverters/UnityConverterTestCaseBase.cs.meta new file mode 100644 index 0000000..e86c6e2 --- /dev/null +++ b/Tests/Runtime/UnityConverters/UnityConverterTestCaseBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 762b7f18faf04283b3893f320ac36b81 +timeCreated: 1725291217 \ No newline at end of file