From bf8e712e0029d5bea45a98271706d7ec7af76199 Mon Sep 17 00:00:00 2001 From: Kurtis Date: Tue, 2 Jun 2026 09:52:37 -0700 Subject: [PATCH 1/3] Add new tests --- .../Tests/Editor/AssemblyExtensionsTests.cs | 37 ++++++ .../Editor/AssemblyExtensionsTests.cs.meta | 11 ++ .../Editor/SerializableDictionaryTests.cs | 87 +++++++++++++ .../SerializableDictionaryTests.cs.meta | 11 ++ .../Tests/Editor/SystemTypeTests.cs | 116 ++++++++++++++++++ .../Tests/Editor/SystemTypeTests.cs.meta | 11 ++ 6 files changed, 273 insertions(+) create mode 100644 org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs create mode 100644 org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs.meta create mode 100644 org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs create mode 100644 org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs.meta create mode 100644 org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs create mode 100644 org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs.meta diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs b/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs new file mode 100644 index 000000000..0ceffcba0 --- /dev/null +++ b/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs @@ -0,0 +1,37 @@ +// Copyright (c) Mixed Reality Toolkit Contributors +// Licensed under the BSD 3-Clause + +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace MixedReality.Toolkit.Core.Tests.EditMode +{ + /// + /// Unit tests for AssemblyExtensions. + /// These run outside of PlayMode and do not require Unity engine initialization. + /// + public class AssemblyExtensionsTests + { + [Test] + public void GetLoadableTypes_ReturnsTypes_ForValidAssembly() + { + Assembly currentAssembly = Assembly.GetExecutingAssembly(); + + IEnumerable types = currentAssembly.GetLoadableTypes(); + + Assert.IsNotNull(types, "GetLoadableTypes should never return null."); + Assert.Greater(types.Count(), 0, "GetLoadableTypes should return the types within the executing assembly."); + Assert.IsTrue(types.Contains(typeof(AssemblyExtensionsTests)), "GetLoadableTypes failed to return known loadable types."); + } + + [Test] + public void GetLoadableTypes_HandlesNullAssembly_Safely() + { + Assembly nullAssembly = null; + Assert.Throws(() => nullAssembly.GetLoadableTypes()); + } + } +} diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs.meta b/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs.meta new file mode 100644 index 000000000..46f2addd4 --- /dev/null +++ b/org.mixedrealitytoolkit.core/Tests/Editor/AssemblyExtensionsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 32e95d882d3d7c44ca4f53530fed61b0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs b/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs new file mode 100644 index 000000000..7f99497c5 --- /dev/null +++ b/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs @@ -0,0 +1,87 @@ +// Copyright (c) Mixed Reality Toolkit Contributors +// Licensed under the BSD 3-Clause + +using NUnit.Framework; +using System.Collections.Generic; +using UnityEngine; + +namespace MixedReality.Toolkit.Core.Tests.EditMode +{ + /// + /// Unit tests for SerializableDictionary. + /// These run outside of PlayMode and do not require Unity engine initialization. + /// + public class SerializableDictionaryTests + { + [Test] + public void Serialization_RestoresDictionary_FromInternalEntries() + { + var dict = new SerializableDictionary(); + dict.Add("Key1", 100); + dict.Add("Key2", 200); + + ISerializationCallbackReceiver receiver = dict; + + // Simulate Unity preparing to serialize the object (populates the internal 'entries' list) + receiver.OnBeforeSerialize(); + + // Clear the base dictionary to simulate starting fresh after deserialization + ((Dictionary)dict).Clear(); + Assert.AreEqual(0, dict.Count, "Base dictionary should be empty before deserialization."); + + // Simulate Unity finishing deserialization (repopulates the dictionary from 'entries') + receiver.OnAfterDeserialize(); + + Assert.AreEqual(2, dict.Count, "Dictionary should have restored 2 items."); + Assert.AreEqual(100, dict["Key1"]); + Assert.AreEqual(200, dict["Key2"]); + } + + [Test] + public void EditorOverride_Clear_RemovesAllSerializedEntries() + { + var dict = new SerializableDictionary(); + dict.Add("Key1", 100); + + ISerializationCallbackReceiver receiver = dict; + receiver.OnBeforeSerialize(); // Populate internal list + + // Call the custom overridden Clear() + dict.Clear(); + + // Attempt to deserialize (which would normally restore Key1 if the internal list wasn't cleared) + receiver.OnAfterDeserialize(); + + Assert.AreEqual(0, dict.Count, "Dictionary should remain empty because the internal serialized entries were cleared."); + } + + [Test] + public void EditorOverride_Remove_RemovesSpecificSerializedEntry() + { + var dict = new SerializableDictionary(); + dict.Add("A", 1); + dict.Add("B", 2); + dict.Add("C", 3); + + ISerializationCallbackReceiver receiver = dict; + receiver.OnBeforeSerialize(); + + // Use the overridden remove method which should also remove from the internal list + bool removedB = dict.Remove("B"); + bool removedC = dict.Remove("C", out int valC); + + // Clear the dictionary and restore from the serialized list to verify they are gone + ((Dictionary)dict).Clear(); + receiver.OnAfterDeserialize(); + + Assert.IsTrue(removedB, "Remove(key) should return true for existing key."); + Assert.IsTrue(removedC, "Remove(key, out val) should return true for existing key."); + Assert.AreEqual(3, valC, "Remove(key, out val) should output the correct value."); + + Assert.AreEqual(1, dict.Count, "Only 1 item should remain after deserialization."); + Assert.IsTrue(dict.ContainsKey("A"), "Key 'A' should have been preserved."); + Assert.IsFalse(dict.ContainsKey("B"), "Key 'B' should have been permanently removed."); + Assert.IsFalse(dict.ContainsKey("C"), "Key 'C' should have been permanently removed."); + } + } +} diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs.meta b/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs.meta new file mode 100644 index 000000000..77e68063f --- /dev/null +++ b/org.mixedrealitytoolkit.core/Tests/Editor/SerializableDictionaryTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2801ac5575b6e5044ac92e55fe647979 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs b/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs new file mode 100644 index 000000000..a1d62be59 --- /dev/null +++ b/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) Mixed Reality Toolkit Contributors +// Licensed under the BSD 3-Clause + +using NUnit.Framework; +using System; +using UnityEngine; +using UnityEngine.TestTools; + +namespace MixedReality.Toolkit.Core.Tests.EditMode +{ + /// + /// Unit tests for SystemType. + /// These run outside of PlayMode and do not require Unity engine initialization. + /// + public class SystemTypeTests + { + [Test] + public void GetReference_FromType_ReturnsValidString() + { + string reference = SystemType.GetReference(typeof(Vector3)); + + Assert.IsFalse(string.IsNullOrEmpty(reference)); + Assert.IsTrue(reference.Contains("UnityEngine.Vector3")); + Assert.IsTrue(reference.Contains("UnityEngine.CoreModule")); + } + + [Test] + public void GetReference_NullOrEmpty_ReturnsEmptyString() + { + Assert.AreEqual(string.Empty, SystemType.GetReference((Type)null)); + Assert.AreEqual(string.Empty, SystemType.GetReference((string)null)); + Assert.AreEqual(string.Empty, SystemType.GetReference(string.Empty)); + } + + [Test] + public void Constructors_SetPropertiesCorrectly() + { + Type targetType = typeof(int); + + // Initialize from Type + SystemType fromType = new SystemType(targetType); + Assert.AreEqual(targetType, fromType.Type); + Assert.AreEqual(SystemType.GetReference(targetType), (string)fromType); + + // Initialize from AssemblyQualifiedName + SystemType fromString = new SystemType(targetType.AssemblyQualifiedName); + Assert.AreEqual(targetType, fromString.Type); + } + + [Test] + public void Constructor_AbstractType_SetsTypeToNull() + { + // SystemType is intentionally designed to nullify abstract types when initialized via string + SystemType fromString = new SystemType(typeof(Array).AssemblyQualifiedName); + Assert.IsNull(fromString.Type, "SystemType should not allow abstract types when initialized from an assembly string."); + } + + [Test] + public void InvalidTypeAssignment_LogsError_ButSetsType() + { + SystemType sysType = new SystemType(typeof(int)); + + // Enums violate the ValidConstraint. SystemType logs an error but still completes the assignment. + LogAssert.Expect(LogType.Error, $"'{typeof(DayOfWeek).FullName}' is not a valid class or struct type."); + + sysType.Type = typeof(DayOfWeek); + + Assert.AreEqual(typeof(DayOfWeek), sysType.Type); + } + + [Test] + public void ImplicitConversions_WorkCorrectly() + { + Type originalType = typeof(string); + + // Type -> SystemType + SystemType sysType = originalType; + Assert.IsNotNull(sysType); + + // SystemType -> Type + Type convertedType = sysType; + Assert.AreEqual(originalType, convertedType); + + // SystemType -> string + string reference = sysType; + Assert.AreEqual(SystemType.GetReference(originalType), reference); + } + + [Test] + public void Equality_MatchesSameTypes() + { + SystemType type1 = new SystemType(typeof(float)); + SystemType type2 = new SystemType(typeof(float)); + SystemType type3 = new SystemType(typeof(double)); + + Assert.IsTrue(type1.Equals(type2)); + Assert.IsFalse(type1.Equals(type3)); + Assert.IsFalse(type1.Equals(null)); + + // HashCodes should match for identical references + Assert.AreEqual(type1.GetHashCode(), type2.GetHashCode()); + } + + [Test] + public void Serialization_RestoresType() + { + var sysType = new SystemType(typeof(int)); + ISerializationCallbackReceiver receiver = sysType; + + // SystemType uses OnAfterDeserialize to re-establish the `type` mapping from the string `reference` + receiver.OnAfterDeserialize(); + + Assert.AreEqual(typeof(int), sysType.Type); + } + } +} diff --git a/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs.meta b/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs.meta new file mode 100644 index 000000000..f6e786970 --- /dev/null +++ b/org.mixedrealitytoolkit.core/Tests/Editor/SystemTypeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bff9d511c00e10941baf652aeb49d861 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 2c2dc6a3312ab843c2521069e314500cfcf32732 Mon Sep 17 00:00:00 2001 From: Kurtis Date: Tue, 2 Jun 2026 10:20:48 -0700 Subject: [PATCH 2/3] Update AssemblyExtensions.cs --- .../Utilities/Extensions/AssemblyExtensions.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/org.mixedrealitytoolkit.core/Utilities/Extensions/AssemblyExtensions.cs b/org.mixedrealitytoolkit.core/Utilities/Extensions/AssemblyExtensions.cs index 8eba33c1e..59e4586da 100644 --- a/org.mixedrealitytoolkit.core/Utilities/Extensions/AssemblyExtensions.cs +++ b/org.mixedrealitytoolkit.core/Utilities/Extensions/AssemblyExtensions.cs @@ -14,10 +14,15 @@ namespace MixedReality.Toolkit public static class AssemblyExtensions { /// - /// Assembly.GetTypes() can throw in some cases. This extension will catch that exception and return only the types which were successfully loaded from the assembly. + /// Assembly.GetTypes() can throw in some cases. This extension will catch that exception and return only the types which were successfully loaded from the assembly. /// public static IEnumerable GetLoadableTypes(this Assembly @this) { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this), "Assembly cannot be null."); + } + try { return @this.GetTypes(); From 16238c39ba56a0ba771630251b86de68132e40e3 Mon Sep 17 00:00:00 2001 From: Kurtis Date: Tue, 2 Jun 2026 10:26:31 -0700 Subject: [PATCH 3/3] Update CHANGELOG.md --- org.mixedrealitytoolkit.core/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/org.mixedrealitytoolkit.core/CHANGELOG.md b/org.mixedrealitytoolkit.core/CHANGELOG.md index df03116c9..4c6da0596 100644 --- a/org.mixedrealitytoolkit.core/CHANGELOG.md +++ b/org.mixedrealitytoolkit.core/CHANGELOG.md @@ -7,6 +7,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed * Updated code style in `HandsSubsystemDescriptor`, `MRTKSubsystemDescriptor`, `DictationSubsystemDescriptor`, and `XRSubsystemHelpers`. [PR #1109](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1109) +* `AssemblyExtensions.GetLoadableTypes` now throws `ArgumentNullException` when called on a null assembly. [PR #1122](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1122) + +### Added + +* Added edit mode tests for `AssemblyExtensions`, `SystemType`, and `SerializableDictionary`. [PR #1122](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1122) ### Fixed