Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5e7353e
add first tests. only one of them works, because:
dsmiller95 Sep 2, 2024
771b279
got 2nd test working, by forcing per-test unique keys
dsmiller95 Sep 2, 2024
274b1a0
fix swallowing exceptions in the async to coroutine converter
dsmiller95 Sep 2, 2024
f930eef
fix hidden exception. the Awaitable WaitForSecondsAsync api is incomp…
dsmiller95 Sep 2, 2024
bbb2cc7
add presumed best-case save file format tests, failing currently
dsmiller95 Sep 2, 2024
efde455
fix the save file format tests to conform w/ current function w/o the…
dsmiller95 Sep 2, 2024
e8952d0
DRY up the test cases
dsmiller95 Sep 2, 2024
9a3ef02
add test for Vector3 serialization. seems to work
dsmiller95 Sep 2, 2024
5572e9a
add test to test the unity json converterrs
dsmiller95 Sep 2, 2024
3c0b8ca
flesh out the test ASMDEF
dsmiller95 Sep 2, 2024
9ee4ef0
add TODO and variant test around a failing, potential bugged, test
dsmiller95 Sep 2, 2024
b8179ec
more docs, and cleanup
dsmiller95 Sep 2, 2024
d8ebdad
remove creation of a saveManager game object from the tests - not nec…
dsmiller95 Sep 2, 2024
51eca0e
add documentation for SaveManager internal state
dsmiller95 Sep 2, 2024
649fdf9
create conditionally-compiling test assembly to test integration with…
dsmiller95 Sep 2, 2024
7503cab
more DRY - functional helper for round-trip testing
dsmiller95 Sep 2, 2024
ac30d33
rename tests. add a new test to make evident the Vector3 save format …
dsmiller95 Sep 2, 2024
723fad0
add comments indicating the thread context of ISaveable methods
dsmiller95 Sep 2, 2024
494db75
pre-review touchup
dsmiller95 Sep 2, 2024
09bf456
use recommendation from unity to test Awaitable async functions. don'…
dsmiller95 Sep 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Runtime/ISaveable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,38 @@ 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.
/// </summary>
/// <remarks>
/// This is invoked off Unity's main thread, cannot use thread unsafe APIs.
/// </remarks>
public string Key { get; }

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This is invoked on Unity's main thread, safe to use thread unsafe APIs.
/// </remarks>
public string Filename { get; }

/// <summary>
/// This is used by the SaveManager class to capture the state of a saveable object.
/// 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.
/// </summary>
/// <remarks>
/// This is invoked off Unity's main thread, cannot use thread unsafe APIs.
/// </remarks>
object CaptureState();

/// <summary>
/// This is used by the SaveManager class to restore the state of a saveable object.
/// 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").
/// </summary>
/// <remarks>
/// This is invoked on Unity's main thread, safe to use thread unsafe APIs.
/// </remarks>
void RestoreState(object state);
}
}
11 changes: 11 additions & 0 deletions Runtime/SaveManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,20 @@ public FileOperation(FileOperationType operationType, string[] filenames)
}

static FileHandler m_fileHandler;
/// <summary>
/// Saveables which have registered themselves inside the manager.
/// </summary>
static Dictionary<string, ISaveable> m_saveables = new();
/// <summary>
/// Temporary working memory used during a <see cref="FileOperationType.Load"/> operation. Stores
/// data which was loaded from a save file and will be restored to ISaveables after the load operation completes.
/// </summary>
static List<SaveableObject> m_loadedSaveables = new();
static Queue<FileOperation> m_fileOperationQueue = new();
/// <summary>
/// A set of all files associated with currently registered ISaveables.
/// Currently unused.
/// </summary>
static HashSet<string> m_files = new();

static bool m_isInitialized;
Expand Down
3 changes: 3 additions & 0 deletions Tests.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Tests/Runtime.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions Tests/Runtime/BUCK.SaveAsync.Tests.asmdef
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions Tests/Runtime/BUCK.SaveAsync.Tests.asmdef.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions Tests/Runtime/TestConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Buck.SaveAsync.Tests
{
public static class TestConstants
{
/// <summary>
/// Asserted against when the namespace is included in json serialization output
/// </summary>
public static string Namespace => "Buck.SaveAsync.Tests";
/// <summary>
/// Asserted against when the assembly is included in json serialization output
/// </summary>
public static string Assembly => "BUCK.SaveAsync.Tests";
}
}
3 changes: 3 additions & 0 deletions Tests/Runtime/TestConstants.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 80 additions & 0 deletions Tests/Runtime/TestRoundTripSaveLoad.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace Buck.SaveAsync.Tests
{
/// <summary>
/// These tests verify round-trip save and load by saving a state, changing the state,
/// and then loading the previously saved state.
/// </summary>
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();
}
}
}
3 changes: 3 additions & 0 deletions Tests/Runtime/TestRoundTripSaveLoad.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

103 changes: 103 additions & 0 deletions Tests/Runtime/TestSaveFileFormat.cs
Original file line number Diff line number Diff line change
@@ -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; }
}

/// <summary>
/// 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.
/// </summary>
public class TestSaveFileFormat : TestCaseBase
{
[UnityTest]
public IEnumerator Test_SaveFormat_NestedDictionary()
{
async Awaitable Impl()
{
// Arrange
var nestedObject = new Dictionary<string, object>
{
{ "key1", "value1" },
{ "key2", 2 },
{
"key3", new Dictionary<string, object>
{
{ "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();
}
}
}
3 changes: 3 additions & 0 deletions Tests/Runtime/TestSaveFileFormat.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Tests/Runtime/TestTools.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions Tests/Runtime/TestTools/InMemoryFileHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Buck.SaveAsync.Tests
{
/// <summary>
/// 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.
/// </summary>
public class InMemoryFileHandler : FileHandler
{
public TimeSpan AllOperationDelay { get; set; } = TimeSpan.Zero;

readonly Dictionary<string, string> m_files = new();

protected override string GetPath(string pathOrFilename) => pathOrFilename;

public override async Task<bool> 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<string> 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);
}
}
}
3 changes: 3 additions & 0 deletions Tests/Runtime/TestTools/InMemoryFileHandler.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading