From 5e70579cd9d709782a0ca7a5de39757e04e48592 Mon Sep 17 00:00:00 2001 From: paq <89paku@gmail.com> Date: Sat, 1 Aug 2026 22:10:46 +0900 Subject: [PATCH 1/7] test: reproduce nested dictionary serialization issue --- .../DictionaryFieldSerializationTest.cs | 64 +++++++++++++++++++ .../DictionaryFieldSerializationTest.cs.meta | 2 + 2 files changed, 66 insertions(+) create mode 100644 tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs create mode 100644 tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs.meta diff --git a/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs new file mode 100644 index 0000000..320fbd6 --- /dev/null +++ b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs @@ -0,0 +1,64 @@ +#if ALCHEMY_SUPPORT_SERIALIZATION +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using Alchemy.Editor.Elements; +using Alchemy.Serialization; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Alchemy.Tests.EditorUI.Editor +{ + [AlchemySerialize] + internal partial class DictionaryCollectionSerializationTarget + { + [AlchemySerializeField, NonSerialized] + public Dictionary arrayValues = new(); + + [AlchemySerializeField, NonSerialized] + public Dictionary> listValues = new(); + } + + public class DictionaryFieldSerializationTest + { + [TestCase(nameof(DictionaryCollectionSerializationTarget.arrayValues))] + [TestCase(nameof(DictionaryCollectionSerializationTarget.listValues))] + public void Test_NestedCollectionEditSurvivesSerializationRoundTrip(string fieldName) + { + var target = new DictionaryCollectionSerializationTarget + { + arrayValues = new Dictionary { [1] = new[] { "before" } }, + listValues = new Dictionary> { [1] = new() { "before" } }, + }; + var callback = (ISerializationCallbackReceiver)target; + callback.OnBeforeSerialize(); + + var fieldInfo = typeof(DictionaryCollectionSerializationTarget).GetField(fieldName); + Assert.That(fieldInfo, Is.Not.Null); + + var reflectionField = new ReflectionField(target, fieldInfo); + var nestedListField = reflectionField.Q(); + Assert.That(nestedListField, Is.Not.Null); + + var dictionary = (IDictionary)fieldInfo.GetValue(target); + var nestedCollection = (IList)dictionary[1]; + nestedCollection[0] = "after"; + + // Simulate the notification emitted after the nested ListField updates its collection. + var notifyOnValueChanged = typeof(ListField) + .GetMethod("NotifyOnValueChanged", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(notifyOnValueChanged, Is.Not.Null); + notifyOnValueChanged.Invoke(nestedListField, null); + + Assert.That(((IList)dictionary[1])[0], Is.EqualTo("after")); + + callback.OnAfterDeserialize(); + + dictionary = (IDictionary)fieldInfo.GetValue(target); + Assert.That(((IList)dictionary[1])[0], Is.EqualTo("after")); + } + } +} +#endif diff --git a/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs.meta b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs.meta new file mode 100644 index 0000000..a4132b4 --- /dev/null +++ b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 047849e0939a4f1e9957d4cc8030ca24 From c3a52853584d44ad0db13859efe5732fa4837dc2 Mon Sep 17 00:00:00 2001 From: paq <89paku@gmail.com> Date: Sat, 1 Aug 2026 22:18:57 +0900 Subject: [PATCH 2/7] test: extract shared editor test utility --- .../DictionaryFieldSerializationTest.cs | 10 ++----- .../Editor/EditorTestUtility.cs | 30 +++++++++++++++++++ .../Editor/EditorTestUtility.cs.meta | 2 ++ 3 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/EditorTestUtility.cs create mode 100644 tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/EditorTestUtility.cs.meta diff --git a/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs index 320fbd6..7822ebd 100644 --- a/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs +++ b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs @@ -2,12 +2,10 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Reflection; using Alchemy.Editor.Elements; using Alchemy.Serialization; using NUnit.Framework; using UnityEngine; -using UnityEngine.UIElements; namespace Alchemy.Tests.EditorUI.Editor { @@ -39,18 +37,14 @@ public void Test_NestedCollectionEditSurvivesSerializationRoundTrip(string field Assert.That(fieldInfo, Is.Not.Null); var reflectionField = new ReflectionField(target, fieldInfo); - var nestedListField = reflectionField.Q(); - Assert.That(nestedListField, Is.Not.Null); + var nestedListField = EditorTestUtility.QueryRequired(reflectionField); var dictionary = (IDictionary)fieldInfo.GetValue(target); var nestedCollection = (IList)dictionary[1]; nestedCollection[0] = "after"; // Simulate the notification emitted after the nested ListField updates its collection. - var notifyOnValueChanged = typeof(ListField) - .GetMethod("NotifyOnValueChanged", BindingFlags.Instance | BindingFlags.NonPublic); - Assert.That(notifyOnValueChanged, Is.Not.Null); - notifyOnValueChanged.Invoke(nestedListField, null); + EditorTestUtility.InvokeNonPublicMethod(nestedListField, "NotifyOnValueChanged"); Assert.That(((IList)dictionary[1])[0], Is.EqualTo("after")); diff --git a/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/EditorTestUtility.cs b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/EditorTestUtility.cs new file mode 100644 index 0000000..c300cb9 --- /dev/null +++ b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/EditorTestUtility.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using NUnit.Framework; +using UnityEngine.UIElements; + +namespace Alchemy.Tests.EditorUI.Editor +{ + internal static class EditorTestUtility + { + public static T QueryRequired(VisualElement root) where T : VisualElement + { + var element = root.Q(); + Assert.That( + element, + Is.Not.Null, + $"Expected a {typeof(T).Name} in {root.GetType().Name}."); + return element; + } + + public static object InvokeNonPublicMethod(object target, string methodName, params object[] arguments) + { + var method = target.GetType() + .GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That( + method, + Is.Not.Null, + $"Expected {target.GetType().Name}.{methodName} to exist."); + return method.Invoke(target, arguments); + } + } +} diff --git a/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/EditorTestUtility.cs.meta b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/EditorTestUtility.cs.meta new file mode 100644 index 0000000..20c893b --- /dev/null +++ b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/EditorTestUtility.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2fa2d055907a40cead331d72bc1db36d From b852d5c4a9994c738cc44d0a9e2baa01b37d9212 Mon Sep 17 00:00:00 2001 From: paq <89paku@gmail.com> Date: Sat, 1 Aug 2026 22:36:19 +0900 Subject: [PATCH 3/7] test: cover dictionary defect reports --- .../GeneratorUtils.cs | 2 + .../NamespaceTests.cs | 51 +++++++++++++++++++ .../DictionaryFieldSerializationTest.cs | 51 ++++++++++++++----- .../Editor/DictionaryFieldTest.cs | 33 ++++++++++++ .../Editor/DictionaryFieldTest.cs.meta | 2 + .../Editor/EditorTestUtility.cs | 35 ++++++++++--- 6 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldTest.cs create mode 100644 tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldTest.cs.meta diff --git a/Alchemy.SourceGenerator.Tests/GeneratorUtils.cs b/Alchemy.SourceGenerator.Tests/GeneratorUtils.cs index e6c920d..98bb307 100644 --- a/Alchemy.SourceGenerator.Tests/GeneratorUtils.cs +++ b/Alchemy.SourceGenerator.Tests/GeneratorUtils.cs @@ -69,6 +69,8 @@ static ImmutableArray LoadFrameworkReferences() namespace UnityEngine { public class Object { } + public class GameObject : Object { } + public class MonoBehaviour : Object { } public interface ISerializationCallbackReceiver { void OnBeforeSerialize(); diff --git a/Alchemy.SourceGenerator.Tests/NamespaceTests.cs b/Alchemy.SourceGenerator.Tests/NamespaceTests.cs index e44e6d2..a136335 100644 --- a/Alchemy.SourceGenerator.Tests/NamespaceTests.cs +++ b/Alchemy.SourceGenerator.Tests/NamespaceTests.cs @@ -134,4 +134,55 @@ public partial class Player await Assert.That(result.AllGeneratedText).Contains("__alchemySerializationData_My_Game_Player"); } + + [Test] + public async Task Dictionary_types_are_globally_qualified_when_root_namespace_is_shadowed() + { + var result = GeneratorUtils.Run(""" + using System; + using System.Collections.Generic; + using Alchemy.Serialization; + using Artillery.Entities; + using Artillery.Entities.Units; + + namespace Artillery.Artillery + { + public sealed class NamespaceCollision { } + } + + namespace Artillery.Entities + { + public enum UnitTeam + { + Ally, + Enemy + } + } + + namespace Artillery.Entities.Units + { + public class BasicUnit : UnityEngine.MonoBehaviour { } + } + + namespace Artillery.Entities.Units.Configuration + { + [AlchemySerialize] + public partial class UnitConfiguration + { + [AlchemySerializeField, NonSerialized] + public Dictionary teamObjects = new(); + + [AlchemySerializeField, NonSerialized] + public Dictionary units = new(); + + [AlchemySerializeField, NonSerialized] + public Dictionary teamUnits = new(); + } + } + """); + + await Assert.That(result.AllGeneratedText).Contains( + "FromJson>"); + await Assert.That(result.DescribeCompilationErrors()).IsEqualTo(""); + } } diff --git a/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs index 7822ebd..5967bfb 100644 --- a/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs +++ b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldSerializationTest.cs @@ -6,6 +6,8 @@ using Alchemy.Serialization; using NUnit.Framework; using UnityEngine; +using UnityEngine.TestTools; +using UnityEngine.UIElements; namespace Alchemy.Tests.EditorUI.Editor { @@ -21,9 +23,21 @@ internal partial class DictionaryCollectionSerializationTarget public class DictionaryFieldSerializationTest { - [TestCase(nameof(DictionaryCollectionSerializationTarget.arrayValues))] - [TestCase(nameof(DictionaryCollectionSerializationTarget.listValues))] - public void Test_NestedCollectionEditSurvivesSerializationRoundTrip(string fieldName) + [UnityTest] + public IEnumerator Test_ArrayValueEditSurvivesSerializationRoundTrip() + { + return Test_NestedCollectionEditSurvivesSerializationRoundTrip( + nameof(DictionaryCollectionSerializationTarget.arrayValues)); + } + + [UnityTest] + public IEnumerator Test_ListValueEditSurvivesSerializationRoundTrip() + { + return Test_NestedCollectionEditSurvivesSerializationRoundTrip( + nameof(DictionaryCollectionSerializationTarget.listValues)); + } + + static IEnumerator Test_NestedCollectionEditSurvivesSerializationRoundTrip(string fieldName) { var target = new DictionaryCollectionSerializationTarget { @@ -37,21 +51,32 @@ public void Test_NestedCollectionEditSurvivesSerializationRoundTrip(string field Assert.That(fieldInfo, Is.Not.Null); var reflectionField = new ReflectionField(target, fieldInfo); - var nestedListField = EditorTestUtility.QueryRequired(reflectionField); + var foldout = EditorTestUtility.QueryRequired(reflectionField); + foldout.value = true; + var window = EditorTestUtility.ShowInWindow(reflectionField); + try + { + yield return null; - var dictionary = (IDictionary)fieldInfo.GetValue(target); - var nestedCollection = (IList)dictionary[1]; - nestedCollection[0] = "after"; + var listView = EditorTestUtility.QueryRequired(reflectionField); + listView.Rebuild(); + yield return null; - // Simulate the notification emitted after the nested ListField updates its collection. - EditorTestUtility.InvokeNonPublicMethod(nestedListField, "NotifyOnValueChanged"); + var textField = EditorTestUtility.QueryRequired(reflectionField); + textField.value = "after"; - Assert.That(((IList)dictionary[1])[0], Is.EqualTo("after")); + var dictionary = (IDictionary)fieldInfo.GetValue(target); + Assert.That(((IList)dictionary[1])[0], Is.EqualTo("after")); - callback.OnAfterDeserialize(); + callback.OnAfterDeserialize(); - dictionary = (IDictionary)fieldInfo.GetValue(target); - Assert.That(((IList)dictionary[1])[0], Is.EqualTo("after")); + dictionary = (IDictionary)fieldInfo.GetValue(target); + Assert.That(((IList)dictionary[1])[0], Is.EqualTo("after")); + } + finally + { + UnityEngine.Object.DestroyImmediate(window); + } } } } diff --git a/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldTest.cs b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldTest.cs new file mode 100644 index 0000000..d86ab13 --- /dev/null +++ b/tests/Alchemy.Tests/Assets/Alchemy.Tests.EditorUI/Editor/DictionaryFieldTest.cs @@ -0,0 +1,33 @@ +using System.Collections; +using System.Collections.Generic; +using Alchemy.Editor.Elements; +using UnityEngine.TestTools; +using UnityEngine.UIElements; + +namespace Alchemy.Tests.EditorUI.Editor +{ + public class DictionaryFieldTest + { + [UnityTest] + public IEnumerator Test_ByteKeyInputCanBeCreated() + { + var field = new DictionaryField(new Dictionary(), "Dictionary"); + var window = EditorTestUtility.ShowInWindow(field); + try + { + yield return null; + + var addButton = EditorTestUtility.QueryRequired