diff --git a/sources/core/Stride.Core.AssemblyProcessor.Tests/Stride.Core.AssemblyProcessor.Tests.csproj b/sources/core/Stride.Core.AssemblyProcessor.Tests/Stride.Core.AssemblyProcessor.Tests.csproj
index 56b90e46e3..8d4208017a 100644
--- a/sources/core/Stride.Core.AssemblyProcessor.Tests/Stride.Core.AssemblyProcessor.Tests.csproj
+++ b/sources/core/Stride.Core.AssemblyProcessor.Tests/Stride.Core.AssemblyProcessor.Tests.csproj
@@ -21,6 +21,10 @@
ASSEMBLY_PROCESSOR;STRIDE_PLATFORM_DESKTOP;TRACE
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/sources/core/Stride.Core.AssemblyProcessor.Tests/TestCecilExtensions.cs b/sources/core/Stride.Core.AssemblyProcessor.Tests/TestCecilExtensions.cs
index 2611b98382..d14ea3957a 100644
--- a/sources/core/Stride.Core.AssemblyProcessor.Tests/TestCecilExtensions.cs
+++ b/sources/core/Stride.Core.AssemblyProcessor.Tests/TestCecilExtensions.cs
@@ -9,19 +9,24 @@ namespace Stride.Core.AssemblyProcessor.Tests;
public class TestCecilExtensions
{
class Nested;
+ class GenericNested;
private readonly BaseAssemblyResolver assemblyResolver = new DefaultAssemblyResolver();
+ private readonly AssemblyDefinition testAssembly;
public TestCecilExtensions()
{
// Add location of current assembly to MonoCecil search path.
- assemblyResolver.AddSearchDirectory(Path.GetDirectoryName(typeof(TestCecilExtensions).Assembly.Location));
+ var assemblyLocation = Path.GetDirectoryName(typeof(TestCecilExtensions).Assembly.Location);
+ assemblyResolver.AddSearchDirectory(assemblyLocation);
+
+ // Load test assembly for Cecil operations
+ testAssembly = AssemblyDefinition.ReadAssembly(typeof(TestCecilExtensions).Assembly.Location);
}
private string GenerateNameCecil(Type type)
{
var typeReference = type.GenerateTypeCecil(assemblyResolver);
-
return typeReference.ConvertAssemblyQualifiedName();
}
@@ -38,31 +43,137 @@ private void CheckGeneratedNames(Type type)
}
[Fact]
- public void TestCecilDotNetAssemblyQualifiedNames()
+ public void TestAssemblyQualifiedNamesPrimitiveTypes()
{
- // Primitive value type
CheckGeneratedNames(typeof(bool));
-
- // Primitive class
+ CheckGeneratedNames(typeof(byte));
+ CheckGeneratedNames(typeof(sbyte));
+ CheckGeneratedNames(typeof(short));
+ CheckGeneratedNames(typeof(ushort));
+ CheckGeneratedNames(typeof(int));
+ CheckGeneratedNames(typeof(uint));
+ CheckGeneratedNames(typeof(long));
+ CheckGeneratedNames(typeof(ulong));
+ CheckGeneratedNames(typeof(float));
+ CheckGeneratedNames(typeof(double));
+ CheckGeneratedNames(typeof(decimal));
+ CheckGeneratedNames(typeof(char));
CheckGeneratedNames(typeof(string));
+ }
- // User class
+ [Fact]
+ public void TestAssemblyQualifiedNamesUserTypes()
+ {
CheckGeneratedNames(typeof(TestCecilExtensions));
+ CheckGeneratedNames(typeof(Nested));
+ }
+ [Fact]
+ public void TestAssemblyQualifiedNamesGenericTypes()
+ {
// Closed generics
+ CheckGeneratedNames(typeof(List));
CheckGeneratedNames(typeof(Dictionary));
+ CheckGeneratedNames(typeof(Dictionary>));
// Open generics
+ CheckGeneratedNames(typeof(List<>));
CheckGeneratedNames(typeof(Dictionary<,>));
+ }
- // Nested types
- CheckGeneratedNames(typeof(Nested));
-
- // Arrays
+ [Fact]
+ public void TestAssemblyQualifiedNamesArrayTypes()
+ {
+ CheckGeneratedNames(typeof(int[]));
CheckGeneratedNames(typeof(string[]));
CheckGeneratedNames(typeof(Dictionary[]));
+ CheckGeneratedNames(typeof(int[,]));
+ CheckGeneratedNames(typeof(int[,,]));
+ }
- // Nullable
+ [Fact]
+ public void TestAssemblyQualifiedNamesNullableTypes()
+ {
CheckGeneratedNames(typeof(bool?));
+ CheckGeneratedNames(typeof(int?));
+ CheckGeneratedNames(typeof(decimal?));
+ }
+
+ [Fact]
+ public void TestConvertCSharpTypeName()
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ Assert.Equal("System.Int32", intType.ConvertCSharp(false));
+
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+ Assert.Equal("System.String", stringType.ConvertCSharp(false));
+ }
+
+ [Fact]
+ public void TestConvertCSharpGenericTypeName()
+ {
+ var listType = testAssembly.MainModule.ImportReference(typeof(List));
+ var result = listType.ConvertCSharp(false);
+ Assert.Contains("System.Collections.Generic.List", result);
+ }
+
+ [Fact]
+ public void TestConvertCSharpEmptyGenericTypeName()
+ {
+ var listType = testAssembly.MainModule.ImportReference(typeof(List<>));
+ var result = listType.ConvertCSharp(true);
+ Assert.Contains("System.Collections.Generic.List<>", result);
+ }
+
+ [Fact]
+ public void TestConvertCSharpArrayTypeName()
+ {
+ var arrayType = testAssembly.MainModule.ImportReference(typeof(int[]));
+ var result = arrayType.ConvertCSharp(false);
+ Assert.Equal("System.Int32[]", result);
+ }
+
+ [Fact]
+ public void TestIsResolvedValueType()
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ Assert.True(intType.IsResolvedValueType());
+
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+ Assert.False(stringType.IsResolvedValueType());
+ }
+
+ [Fact]
+ public void TestMakeGenericType()
+ {
+ var listTypeDef = testAssembly.MainModule.ImportReference(typeof(List<>)).Resolve();
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+
+ var genericInstance = listTypeDef.MakeGenericType(intType);
+
+ Assert.IsType(genericInstance);
+ var git = (GenericInstanceType)genericInstance;
+ Assert.Single(git.GenericArguments);
+ Assert.Equal(intType.FullName, git.GenericArguments[0].FullName);
+ }
+
+ [Fact]
+ public void TestMakeGenericTypeInvalidArgumentCount()
+ {
+ var listTypeDef = testAssembly.MainModule.ImportReference(typeof(List<>)).Resolve();
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+
+ // List expects 1 type argument, not 2
+ Assert.Throws(() => listTypeDef.MakeGenericType(intType, stringType));
+ }
+
+ [Fact]
+ public void TestMakeGenericTypeNoArguments()
+ {
+ var listTypeDef = testAssembly.MainModule.ImportReference(typeof(List<>)).Resolve();
+
+ // List expects 1 type argument, providing 0 should throw
+ Assert.Throws(() => listTypeDef.MakeGenericType());
}
}
diff --git a/sources/core/Stride.Core.AssemblyProcessor.Tests/TestCecilExtensionsAdvanced.cs b/sources/core/Stride.Core.AssemblyProcessor.Tests/TestCecilExtensionsAdvanced.cs
new file mode 100644
index 0000000000..528c034d5c
--- /dev/null
+++ b/sources/core/Stride.Core.AssemblyProcessor.Tests/TestCecilExtensionsAdvanced.cs
@@ -0,0 +1,346 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Xunit;
+
+namespace Stride.Core.AssemblyProcessor.Tests;
+
+public class TestCecilExtensionsAdvanced
+{
+ private readonly AssemblyDefinition testAssembly;
+ private readonly BaseAssemblyResolver assemblyResolver;
+
+ public TestCecilExtensionsAdvanced()
+ {
+ assemblyResolver = new DefaultAssemblyResolver();
+ var assemblyLocation = Path.GetDirectoryName(typeof(TestCecilExtensionsAdvanced).Assembly.Location);
+ assemblyResolver.AddSearchDirectory(assemblyLocation);
+ testAssembly = AssemblyDefinition.ReadAssembly(typeof(TestCecilExtensionsAdvanced).Assembly.Location);
+ }
+
+ [Fact]
+ public void TestGetEmptyConstructorPublic()
+ {
+ var listType = testAssembly.MainModule.ImportReference(typeof(List)).Resolve();
+ var constructor = listType.GetEmptyConstructor();
+
+ Assert.NotNull(constructor);
+ Assert.True(constructor.IsConstructor);
+ Assert.Empty(constructor.Parameters);
+ Assert.True(constructor.IsPublic);
+ }
+
+ [Fact]
+ public void TestGetEmptyConstructorPrivate()
+ {
+ // Create a type definition with only a private constructor for testing
+ var testType = new TypeDefinition("Test", "PrivateConstructorTest",
+ Mono.Cecil.TypeAttributes.Class | Mono.Cecil.TypeAttributes.Public);
+
+ var privateCtor = new MethodDefinition(".ctor",
+ Mono.Cecil.MethodAttributes.Private | Mono.Cecil.MethodAttributes.HideBySig |
+ Mono.Cecil.MethodAttributes.SpecialName | Mono.Cecil.MethodAttributes.RTSpecialName,
+ testAssembly.MainModule.TypeSystem.Void);
+ testType.Methods.Add(privateCtor);
+
+ // Should not find without allowPrivate
+ Assert.Null(testType.GetEmptyConstructor(false));
+
+ // Should find with allowPrivate
+ Assert.NotNull(testType.GetEmptyConstructor(true));
+ }
+
+ [Fact]
+ public void TestGetEmptyConstructorNoConstructor()
+ {
+ var testType = new TypeDefinition("Test", "NoConstructorTest",
+ Mono.Cecil.TypeAttributes.Class | Mono.Cecil.TypeAttributes.Public);
+
+ Assert.Null(testType.GetEmptyConstructor());
+ }
+
+ [Fact]
+ public void TestMakeGenericMethod()
+ {
+ // Get a generic method reference
+ var listType = testAssembly.MainModule.ImportReference(typeof(List<>));
+ var toArrayMethod = listType.Resolve().Methods.First(m => m.Name == "ToArray");
+
+ if (toArrayMethod.HasGenericParameters)
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ var genericMethod = toArrayMethod.MakeGenericMethod(intType);
+
+ Assert.IsType(genericMethod);
+ var gim = (GenericInstanceMethod)genericMethod;
+ Assert.Single(gim.GenericArguments);
+ }
+ }
+
+ [Fact]
+ public void TestMakeGenericMethodInvalidArgumentCount()
+ {
+ var listType = testAssembly.MainModule.ImportReference(typeof(List<>));
+ var toArrayMethod = listType.Resolve().Methods.First(m => m.Name == "ToArray");
+
+ if (toArrayMethod.HasGenericParameters && toArrayMethod.GenericParameters.Count == 1)
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+
+ // Providing wrong number of type arguments
+ Assert.Throws(() => toArrayMethod.MakeGenericMethod(intType, stringType));
+ }
+ }
+
+ [Fact]
+ public void TestMakeGenericField()
+ {
+ var dictType = testAssembly.MainModule.ImportReference(typeof(Dictionary<,>)).Resolve();
+ var field = dictType.Fields.FirstOrDefault();
+
+ if (field != null)
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+
+ var genericField = field.MakeGeneric(intType, stringType);
+
+ Assert.NotNull(genericField);
+ Assert.IsType(genericField.DeclaringType);
+ }
+ }
+
+ [Fact]
+ public void TestMakeGenericNoArguments()
+ {
+ var listType = testAssembly.MainModule.ImportReference(typeof(List<>)).Resolve();
+ var addMethod = listType.Methods.First(m => m.Name == "Add");
+
+ var result = addMethod.MakeGeneric();
+
+ // Should return the same reference when no arguments provided
+ Assert.Same(addMethod, result);
+ }
+
+ [Fact]
+ public void TestOpenModuleConstructor()
+ {
+ var assembly = AssemblyDefinition.CreateAssembly(
+ new AssemblyNameDefinition("TestAssembly", new Version(1, 0, 0, 0)),
+ "TestModule",
+ ModuleKind.Dll);
+
+ var staticCtor = assembly.OpenModuleConstructor(out var returnInstruction);
+
+ Assert.NotNull(staticCtor);
+ Assert.True(staticCtor.IsStatic);
+ Assert.True(staticCtor.IsConstructor);
+ Assert.NotNull(returnInstruction);
+ Assert.Equal(OpCodes.Ret, returnInstruction.OpCode);
+ }
+
+ [Fact]
+ public void TestOpenModuleConstructorExisting()
+ {
+ var assembly = AssemblyDefinition.CreateAssembly(
+ new AssemblyNameDefinition("TestAssembly", new Version(1, 0, 0, 0)),
+ "TestModule",
+ ModuleKind.Dll);
+
+ // Call twice to ensure it returns existing constructor
+ var staticCtor1 = assembly.OpenModuleConstructor(out var returnInstruction1);
+ var staticCtor2 = assembly.OpenModuleConstructor(out var returnInstruction2);
+
+ Assert.Same(staticCtor1, staticCtor2);
+ Assert.Same(returnInstruction1, returnInstruction2);
+ }
+
+ [Fact]
+ public void TestFindCorlibAssembly()
+ {
+ var corlibAssembly = CecilExtensions.FindCorlibAssembly(testAssembly);
+
+ Assert.NotNull(corlibAssembly);
+ // Should be either mscorlib or System.Runtime depending on target framework
+ Assert.True(
+ corlibAssembly.Name.Name.Equals("mscorlib", StringComparison.OrdinalIgnoreCase) ||
+ corlibAssembly.Name.Name.Equals("System.Runtime", StringComparison.OrdinalIgnoreCase),
+ $"Expected mscorlib or System.Runtime, got {corlibAssembly.Name.Name}");
+ }
+
+ [Fact]
+ public void TestFindCollectionsAssembly()
+ {
+ var collectionsAssembly = CecilExtensions.FindCollectionsAssembly(testAssembly);
+
+ Assert.NotNull(collectionsAssembly);
+ // Should contain collection types
+ Assert.True(
+ collectionsAssembly.Name.Name.Equals("mscorlib", StringComparison.OrdinalIgnoreCase) ||
+ collectionsAssembly.Name.Name.Equals("System.Collections", StringComparison.OrdinalIgnoreCase),
+ $"Expected mscorlib or System.Collections, got {collectionsAssembly.Name.Name}");
+ }
+
+ [Fact]
+ public void TestFindReflectionAssembly()
+ {
+ var reflectionAssembly = CecilExtensions.FindReflectionAssembly(testAssembly);
+
+ Assert.NotNull(reflectionAssembly);
+ // Should contain reflection types
+ Assert.True(
+ reflectionAssembly.Name.Name.Equals("mscorlib", StringComparison.OrdinalIgnoreCase) ||
+ reflectionAssembly.Name.Name.Equals("System.Reflection", StringComparison.OrdinalIgnoreCase),
+ $"Expected mscorlib or System.Reflection, got {reflectionAssembly.Name.Name}");
+ }
+
+ [Fact]
+ public void TestGetTypeResolved()
+ {
+ // Use a type that exists in the test assembly's module
+ var testType = testAssembly.MainModule.GetTypeResolved(typeof(TestCecilExtensionsAdvanced).FullName);
+
+ Assert.NotNull(testType);
+ Assert.Equal(typeof(TestCecilExtensionsAdvanced).FullName, testType.FullName);
+ }
+
+ [Fact]
+ public void TestGetTypeResolvedWithNamespace()
+ {
+ var testType = testAssembly.MainModule.GetTypeResolved(
+ typeof(TestCecilExtensionsAdvanced).Namespace,
+ typeof(TestCecilExtensionsAdvanced).Name);
+
+ Assert.NotNull(testType);
+ Assert.Equal(typeof(TestCecilExtensionsAdvanced).Namespace, testType.Namespace);
+ Assert.Equal(typeof(TestCecilExtensionsAdvanced).Name, testType.Name);
+ }
+
+ [Fact]
+ public void TestGenerateGenericsOpen()
+ {
+ var listType = testAssembly.MainModule.ImportReference(typeof(List<>));
+ var generics = listType.GenerateGenerics(true);
+
+ Assert.Equal("<>", generics);
+ }
+
+ [Fact]
+ public void TestGenerateGenericsClosed()
+ {
+ var listType = testAssembly.MainModule.ImportReference(typeof(List));
+ var generics = listType.GenerateGenerics(false);
+
+ Assert.Contains("<", generics);
+ Assert.Contains(">", generics);
+ Assert.Contains("Int32", generics);
+ }
+
+ [Fact]
+ public void TestGenerateGenericsNonGeneric()
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ var generics = intType.GenerateGenerics();
+
+ Assert.Equal(string.Empty, generics);
+ }
+
+ [Fact]
+ public void TestChangeGenericInstanceType()
+ {
+ var listIntType = (GenericInstanceType)testAssembly.MainModule.ImportReference(typeof(List));
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+
+ var newGenericArgs = new[] { stringType };
+ var result = listIntType.ChangeGenericInstanceType(listIntType.ElementType, newGenericArgs);
+
+ Assert.IsType(result);
+ Assert.Single(result.GenericArguments);
+ Assert.Equal(stringType.FullName, result.GenericArguments[0].FullName);
+ }
+
+ [Fact]
+ public void TestChangeGenericInstanceTypeSameArguments()
+ {
+ var listIntType = (GenericInstanceType)testAssembly.MainModule.ImportReference(typeof(List));
+
+ var result = listIntType.ChangeGenericInstanceType(listIntType.ElementType, listIntType.GenericArguments);
+
+ // Should return same instance when nothing changed
+ Assert.Same(listIntType, result);
+ }
+
+ [Fact]
+ public void TestChangeArrayType()
+ {
+ var intArrayType = new ArrayType(testAssembly.MainModule.TypeSystem.Int32, 1);
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+
+ var result = intArrayType.ChangeArrayType(stringType, 1);
+
+ Assert.Equal(stringType.FullName, result.ElementType.FullName);
+ Assert.Equal(1, result.Rank);
+ }
+
+ [Fact]
+ public void TestChangeArrayTypeRank()
+ {
+ var intArrayType = new ArrayType(testAssembly.MainModule.TypeSystem.Int32, 1);
+
+ var result = intArrayType.ChangeArrayType(testAssembly.MainModule.TypeSystem.Int32, 2);
+
+ Assert.Equal(2, result.Rank);
+ }
+
+ [Fact]
+ public void TestChangeArrayTypeSame()
+ {
+ var intArrayType = new ArrayType(testAssembly.MainModule.TypeSystem.Int32, 1);
+
+ var result = intArrayType.ChangeArrayType(testAssembly.MainModule.TypeSystem.Int32, 1);
+
+ // Should return same instance when nothing changed
+ Assert.Same(intArrayType, result);
+ }
+
+ [Fact]
+ public void TestAddRangeToList()
+ {
+ var list = new List { 1, 2, 3 };
+ var itemsToAdd = new[] { 4, 5, 6 };
+
+ list.AddRange(itemsToAdd);
+
+ Assert.Equal(6, list.Count);
+ Assert.Equal(new[] { 1, 2, 3, 4, 5, 6 }, list);
+ }
+
+ [Fact]
+ public void TestAddRangeToCollection()
+ {
+ ICollection collection = new List { 1, 2, 3 };
+ var itemsToAdd = new[] { 4, 5, 6 };
+
+ collection.AddRange(itemsToAdd);
+
+ Assert.Equal(6, collection.Count);
+ }
+
+ [Fact]
+ public void TestContainsGenericParameter()
+ {
+ var listTypeDef = testAssembly.MainModule.ImportReference(typeof(List<>)).Resolve();
+
+ // Generic type definition has generic parameters but they are defined (not unresolved)
+ // The method checks if there are unresolved generic parameters in the type hierarchy
+ // Let's test with a generic instance that has a generic parameter
+ var genericParam = new GenericParameter("T", listTypeDef);
+ var genericInstance = new GenericInstanceType(listTypeDef);
+ genericInstance.GenericArguments.Add(genericParam);
+
+ Assert.True(genericInstance.ContainsGenericParameter());
+ }
+}
diff --git a/sources/core/Stride.Core.AssemblyProcessor.Tests/TestTypeReferenceEqualityComparer.cs b/sources/core/Stride.Core.AssemblyProcessor.Tests/TestTypeReferenceEqualityComparer.cs
new file mode 100644
index 0000000000..7f6be7d79c
--- /dev/null
+++ b/sources/core/Stride.Core.AssemblyProcessor.Tests/TestTypeReferenceEqualityComparer.cs
@@ -0,0 +1,127 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Mono.Cecil;
+using Xunit;
+
+namespace Stride.Core.AssemblyProcessor.Tests;
+
+public class TestTypeReferenceEqualityComparer
+{
+ private readonly AssemblyDefinition testAssembly;
+
+ public TestTypeReferenceEqualityComparer()
+ {
+ testAssembly = AssemblyDefinition.ReadAssembly(typeof(TestTypeReferenceEqualityComparer).Assembly.Location);
+ }
+
+ [Fact]
+ public void TestDefaultInstance()
+ {
+ Assert.NotNull(TypeReferenceEqualityComparer.Default);
+ Assert.Same(TypeReferenceEqualityComparer.Default, TypeReferenceEqualityComparer.Default);
+ }
+
+ [Fact]
+ public void TestEqualsSameType()
+ {
+ var intType1 = testAssembly.MainModule.TypeSystem.Int32;
+ var intType2 = testAssembly.MainModule.TypeSystem.Int32;
+
+ Assert.True(TypeReferenceEqualityComparer.Default.Equals(intType1, intType2));
+ }
+
+ [Fact]
+ public void TestEqualsDifferentTypes()
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+
+ Assert.False(TypeReferenceEqualityComparer.Default.Equals(intType, stringType));
+ }
+
+ [Fact]
+ public void TestEqualsImportedTypes()
+ {
+ var listType1 = testAssembly.MainModule.ImportReference(typeof(List));
+ var listType2 = testAssembly.MainModule.ImportReference(typeof(List));
+
+ Assert.True(TypeReferenceEqualityComparer.Default.Equals(listType1, listType2));
+ }
+
+ [Fact]
+ public void TestEqualsDifferentGenericArguments()
+ {
+ var listIntType = testAssembly.MainModule.ImportReference(typeof(List));
+ var listStringType = testAssembly.MainModule.ImportReference(typeof(List));
+
+ Assert.False(TypeReferenceEqualityComparer.Default.Equals(listIntType, listStringType));
+ }
+
+ [Fact]
+ public void TestGetHashCodeConsistency()
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+
+ var hash1 = TypeReferenceEqualityComparer.Default.GetHashCode(intType);
+ var hash2 = TypeReferenceEqualityComparer.Default.GetHashCode(intType);
+
+ Assert.Equal(hash1, hash2);
+ }
+
+ [Fact]
+ public void TestGetHashCodeEqualTypes()
+ {
+ var intType1 = testAssembly.MainModule.TypeSystem.Int32;
+ var intType2 = testAssembly.MainModule.TypeSystem.Int32;
+
+ var hash1 = TypeReferenceEqualityComparer.Default.GetHashCode(intType1);
+ var hash2 = TypeReferenceEqualityComparer.Default.GetHashCode(intType2);
+
+ Assert.Equal(hash1, hash2);
+ }
+
+ [Fact]
+ public void TestGetHashCodeDifferentTypes()
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+
+ var hash1 = TypeReferenceEqualityComparer.Default.GetHashCode(intType);
+ var hash2 = TypeReferenceEqualityComparer.Default.GetHashCode(stringType);
+
+ Assert.NotEqual(hash1, hash2);
+ }
+
+ [Fact]
+ public void TestUsageInHashSet()
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+ var intType2 = testAssembly.MainModule.TypeSystem.Int32;
+
+ var hashSet = new HashSet(TypeReferenceEqualityComparer.Default);
+
+ Assert.True(hashSet.Add(intType));
+ Assert.True(hashSet.Add(stringType));
+ Assert.False(hashSet.Add(intType2)); // Should not add duplicate
+
+ Assert.Equal(2, hashSet.Count);
+ }
+
+ [Fact]
+ public void TestUsageInDictionary()
+ {
+ var intType = testAssembly.MainModule.TypeSystem.Int32;
+ var stringType = testAssembly.MainModule.TypeSystem.String;
+
+ var dictionary = new Dictionary(TypeReferenceEqualityComparer.Default);
+
+ dictionary[intType] = "Integer";
+ dictionary[stringType] = "String";
+
+ Assert.Equal(2, dictionary.Count);
+ Assert.Equal("Integer", dictionary[intType]);
+ Assert.Equal("String", dictionary[stringType]);
+ }
+}
diff --git a/sources/core/Stride.Core.AssemblyProcessor.Tests/TestUtilities.cs b/sources/core/Stride.Core.AssemblyProcessor.Tests/TestUtilities.cs
new file mode 100644
index 0000000000..1ccbd00528
--- /dev/null
+++ b/sources/core/Stride.Core.AssemblyProcessor.Tests/TestUtilities.cs
@@ -0,0 +1,207 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Xunit;
+
+namespace Stride.Core.AssemblyProcessor.Tests;
+
+public class TestUtilities
+{
+ [Fact]
+ public void TestBuildValidClassNameSimple()
+ {
+ Assert.Equal("MyClass", Utilities.BuildValidClassName("MyClass"));
+ Assert.Equal("my_class", Utilities.BuildValidClassName("my_class"));
+ }
+
+ [Fact]
+ public void TestBuildValidClassNameWithInvalidCharacters()
+ {
+ Assert.Equal("My_Class", Utilities.BuildValidClassName("My Class"));
+ Assert.Equal("My_Class", Utilities.BuildValidClassName("My-Class"));
+ Assert.Equal("My_Class", Utilities.BuildValidClassName("My+Class"));
+ Assert.Equal("My_Class_Name", Utilities.BuildValidClassName("My@Class#Name"));
+ Assert.Equal("Test___Data", Utilities.BuildValidClassName("Test!@#Data"));
+ }
+
+ [Fact]
+ public void TestBuildValidClassNameStartsWithNumber()
+ {
+ Assert.Equal("_123Class", Utilities.BuildValidClassName("123Class"));
+ Assert.Equal("_7Days", Utilities.BuildValidClassName("7Days"));
+ }
+
+ [Fact]
+ public void TestBuildValidClassNameReservedKeywords()
+ {
+ Assert.Equal("class_", Utilities.BuildValidClassName("class"));
+ Assert.Equal("int_", Utilities.BuildValidClassName("int"));
+ Assert.Equal("string_", Utilities.BuildValidClassName("string"));
+ Assert.Equal("namespace_", Utilities.BuildValidClassName("namespace"));
+ Assert.Equal("public_", Utilities.BuildValidClassName("public"));
+ Assert.Equal("private_", Utilities.BuildValidClassName("private"));
+ Assert.Equal("void_", Utilities.BuildValidClassName("void"));
+ }
+
+ [Fact]
+ public void TestBuildValidClassNameWithCustomReplacementCharacter()
+ {
+ Assert.Equal("My@Class", Utilities.BuildValidClassName("My Class", '@'));
+ Assert.Equal("Test$Name", Utilities.BuildValidClassName("Test-Name", '$'));
+ }
+
+ [Fact]
+ public void TestBuildValidClassNameWithAdditionalReservedWords()
+ {
+ var additionalReservedWords = new[] { "CustomReserved", "AnotherReserved" };
+
+ Assert.Equal("CustomReserved_", Utilities.BuildValidClassName("CustomReserved", additionalReservedWords));
+ Assert.Equal("AnotherReserved_", Utilities.BuildValidClassName("AnotherReserved", additionalReservedWords));
+ Assert.Equal("NormalName", Utilities.BuildValidClassName("NormalName", additionalReservedWords));
+ }
+
+ [Fact]
+ public void TestBuildValidClassNameSpecialCharacters()
+ {
+ Assert.Equal("a_b_c", Utilities.BuildValidClassName("a;b,c"));
+ Assert.Equal("test_query", Utilities.BuildValidClassName("test?query"));
+ Assert.Equal("angle_brackets_", Utilities.BuildValidClassName("angle"));
+ Assert.Equal("quotes_", Utilities.BuildValidClassName("quotes\""));
+ }
+
+ [Fact]
+ public void TestBuildValidNamespaceNameSimple()
+ {
+ Assert.Equal("MyNamespace", Utilities.BuildValidNamespaceName("MyNamespace"));
+ Assert.Equal("My.Namespace", Utilities.BuildValidNamespaceName("My.Namespace"));
+ }
+
+ [Fact]
+ public void TestBuildValidNamespaceNameWithInvalidCharacters()
+ {
+ Assert.Equal("My_Namespace", Utilities.BuildValidNamespaceName("My Namespace"));
+ Assert.Equal("My_Namespace", Utilities.BuildValidNamespaceName("My-Namespace"));
+ }
+
+ [Fact]
+ public void TestBuildValidNamespaceNameStartsWithNumber()
+ {
+ Assert.Equal("_123Namespace", Utilities.BuildValidNamespaceName("123Namespace"));
+ }
+
+ [Fact]
+ public void TestBuildValidNamespaceNameReservedKeywords()
+ {
+ Assert.Equal("class_", Utilities.BuildValidNamespaceName("class"));
+ Assert.Equal("namespace_", Utilities.BuildValidNamespaceName("namespace"));
+ }
+
+ [Fact]
+ public void TestBuildValidNamespaceNameDotFollowedByNumber()
+ {
+ // Dots followed by numbers should be replaced
+ Assert.Equal("Version_2_0", Utilities.BuildValidNamespaceName("Version.2.0"));
+ }
+
+ [Fact]
+ public void TestBuildValidNamespaceNameWithCustomReplacementCharacter()
+ {
+ Assert.Equal("My@Namespace", Utilities.BuildValidNamespaceName("My Namespace", '@'));
+ }
+
+ [Fact]
+ public void TestBuildValidNamespaceNameWithAdditionalReservedWords()
+ {
+ var additionalReservedWords = new[] { "CustomReserved" };
+ Assert.Equal("CustomReserved_", Utilities.BuildValidNamespaceName("CustomReserved", additionalReservedWords));
+ }
+
+ [Fact]
+ public void TestBuildValidProjectNameSimple()
+ {
+ Assert.Equal("MyProject", Utilities.BuildValidProjectName("MyProject"));
+ Assert.Equal("My.Project", Utilities.BuildValidProjectName("My.Project"));
+ }
+
+ [Fact]
+ public void TestBuildValidProjectNameWithInvalidCharacters()
+ {
+ Assert.Equal("My_Project", Utilities.BuildValidProjectName("My=Project"));
+ Assert.Equal("Path_To_Project", Utilities.BuildValidProjectName("Path/To/Project"));
+ Assert.Equal("Query_String", Utilities.BuildValidProjectName("Query?String"));
+ Assert.Equal("Colon_Name", Utilities.BuildValidProjectName("Colon:Name"));
+ Assert.Equal("Ampersand_Name", Utilities.BuildValidProjectName("Ampersand&Name"));
+ Assert.Equal("Asterisk_Name", Utilities.BuildValidProjectName("Asterisk*Name"));
+ Assert.Equal("Less_Greater_", Utilities.BuildValidProjectName("Less"));
+ Assert.Equal("Pipe_Name", Utilities.BuildValidProjectName("Pipe|Name"));
+ Assert.Equal("Hash_Name", Utilities.BuildValidProjectName("Hash#Name"));
+ Assert.Equal("Percent_Name", Utilities.BuildValidProjectName("Percent%Name"));
+ Assert.Equal("Quote_", Utilities.BuildValidProjectName("Quote\""));
+ }
+
+ [Fact]
+ public void TestBuildValidProjectNameWithCustomReplacementCharacter()
+ {
+ Assert.Equal("My-Project", Utilities.BuildValidProjectName("My/Project", '-'));
+ }
+
+ [Fact]
+ public void TestBuildValidFileNameSimple()
+ {
+ Assert.Equal("MyFile", Utilities.BuildValidFileName("MyFile"));
+ Assert.Equal("My File", Utilities.BuildValidFileName("My File")); // Spaces are allowed in filenames
+ }
+
+ [Fact]
+ public void TestBuildValidFileNameWithInvalidCharacters()
+ {
+ Assert.Equal("My_File", Utilities.BuildValidFileName("My=File"));
+ Assert.Equal("Path_To_File", Utilities.BuildValidFileName("Path/To/File"));
+ Assert.Equal("Query_String", Utilities.BuildValidFileName("Query?String"));
+ Assert.Equal("Colon_Name", Utilities.BuildValidFileName("Colon:Name"));
+ Assert.Equal("Ampersand_Name", Utilities.BuildValidFileName("Ampersand&Name"));
+ Assert.Equal("Exclamation_Name", Utilities.BuildValidFileName("Exclamation!Name"));
+ Assert.Equal("Dot__", Utilities.BuildValidFileName("Dot.*")); // Both . and * are invalid
+ Assert.Equal("Less_Greater_", Utilities.BuildValidFileName("Less"));
+ Assert.Equal("Pipe_Name", Utilities.BuildValidFileName("Pipe|Name"));
+ Assert.Equal("Hash_Name", Utilities.BuildValidFileName("Hash#Name"));
+ Assert.Equal("Percent_Name", Utilities.BuildValidFileName("Percent%Name"));
+ Assert.Equal("Quote_", Utilities.BuildValidFileName("Quote\""));
+ }
+
+ [Fact]
+ public void TestBuildValidFileNameWithCustomReplacementCharacter()
+ {
+ Assert.Equal("My-File", Utilities.BuildValidFileName("My/File", '-'));
+ }
+
+ [Fact]
+ public void TestBuildValidClassNameEdgeCases()
+ {
+ // Underscore is valid, should not be changed
+ Assert.Equal("_", Utilities.BuildValidClassName("_"));
+
+ // Multiple consecutive invalid characters
+ Assert.Equal("Test____Name", Utilities.BuildValidClassName("Test!@#$Name"));
+
+ // All reserved characters (19 characters in the input string)
+ Assert.Equal("___________________", Utilities.BuildValidClassName(" -;',+*|!`~@#$%^&?"));
+ }
+
+ [Fact]
+ public void TestBuildValidNamespaceNamePreservesDots()
+ {
+ // Dots should be preserved in namespace names (except when followed by numbers)
+ Assert.Equal("System.Collections.Generic", Utilities.BuildValidNamespaceName("System.Collections.Generic"));
+ }
+
+ [Fact]
+ public void TestBuildValidClassNameCommonPatterns()
+ {
+ // Common naming patterns
+ Assert.Equal("IMyInterface", Utilities.BuildValidClassName("IMyInterface"));
+ Assert.Equal("_privateField", Utilities.BuildValidClassName("_privateField"));
+ Assert.Equal("MyClass123", Utilities.BuildValidClassName("MyClass123"));
+ Assert.Equal("CONSTANT_VALUE", Utilities.BuildValidClassName("CONSTANT_VALUE"));
+ }
+}
diff --git a/sources/core/Stride.Core.Design.Tests/Collections/TestHybridDictionary.cs b/sources/core/Stride.Core.Design.Tests/Collections/TestHybridDictionary.cs
index 81b427b550..269a36bc8f 100644
--- a/sources/core/Stride.Core.Design.Tests/Collections/TestHybridDictionary.cs
+++ b/sources/core/Stride.Core.Design.Tests/Collections/TestHybridDictionary.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Stride.Core.Collections;
diff --git a/sources/core/Stride.Core.Design.Tests/Extensions/TestDictionaryExtensions.cs b/sources/core/Stride.Core.Design.Tests/Extensions/TestDictionaryExtensions.cs
index 2faa0aa9f3..503bab6fcb 100644
--- a/sources/core/Stride.Core.Design.Tests/Extensions/TestDictionaryExtensions.cs
+++ b/sources/core/Stride.Core.Design.Tests/Extensions/TestDictionaryExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Stride.Core.Extensions;
diff --git a/sources/core/Stride.Core.Design.Tests/Extensions/TestEnumExtensions.cs b/sources/core/Stride.Core.Design.Tests/Extensions/TestEnumExtensions.cs
index 06857f3128..48f00bb6bd 100644
--- a/sources/core/Stride.Core.Design.Tests/Extensions/TestEnumExtensions.cs
+++ b/sources/core/Stride.Core.Design.Tests/Extensions/TestEnumExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Stride.Core.Extensions;
diff --git a/sources/core/Stride.Core.Design.Tests/Extensions/TestListExtensions.cs b/sources/core/Stride.Core.Design.Tests/Extensions/TestListExtensions.cs
index 57df598782..5a2b96fb53 100644
--- a/sources/core/Stride.Core.Design.Tests/Extensions/TestListExtensions.cs
+++ b/sources/core/Stride.Core.Design.Tests/Extensions/TestListExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Stride.Core.Extensions;
diff --git a/sources/core/Stride.Core.Design.Tests/TestAbsoluteId.cs b/sources/core/Stride.Core.Design.Tests/TestAbsoluteId.cs
index 8c02fe0554..739b259457 100644
--- a/sources/core/Stride.Core.Design.Tests/TestAbsoluteId.cs
+++ b/sources/core/Stride.Core.Design.Tests/TestAbsoluteId.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Stride.Core.Assets;
diff --git a/sources/core/Stride.Core.Design.Tests/TestPackageVersionRange.cs b/sources/core/Stride.Core.Design.Tests/TestPackageVersionRange.cs
index c15e895192..eb836baf83 100644
--- a/sources/core/Stride.Core.Design.Tests/TestPackageVersionRange.cs
+++ b/sources/core/Stride.Core.Design.Tests/TestPackageVersionRange.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Xunit;
diff --git a/sources/core/Stride.Core.Design.Tests/TestStringSpan.cs b/sources/core/Stride.Core.Design.Tests/TestStringSpan.cs
index 0b125a0b50..132ab1f5ee 100644
--- a/sources/core/Stride.Core.Design.Tests/TestStringSpan.cs
+++ b/sources/core/Stride.Core.Design.Tests/TestStringSpan.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Xunit;
diff --git a/sources/core/Stride.Core.Design.Tests/TestStringSpanExtensions.cs b/sources/core/Stride.Core.Design.Tests/TestStringSpanExtensions.cs
index 9fefe13e5f..ba6a19de75 100644
--- a/sources/core/Stride.Core.Design.Tests/TestStringSpanExtensions.cs
+++ b/sources/core/Stride.Core.Design.Tests/TestStringSpanExtensions.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Xunit;
diff --git a/sources/core/Stride.Core.Design.Tests/TypeConverters/TestColorConverter.cs b/sources/core/Stride.Core.Design.Tests/TypeConverters/TestColorConverter.cs
index 645462fbee..6ad4d7997b 100644
--- a/sources/core/Stride.Core.Design.Tests/TypeConverters/TestColorConverter.cs
+++ b/sources/core/Stride.Core.Design.Tests/TypeConverters/TestColorConverter.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using System.Collections;
diff --git a/sources/core/Stride.Core.Design.Tests/TypeConverters/TestMatrixConverter.cs b/sources/core/Stride.Core.Design.Tests/TypeConverters/TestMatrixConverter.cs
index 6e6f3690aa..484567960c 100644
--- a/sources/core/Stride.Core.Design.Tests/TypeConverters/TestMatrixConverter.cs
+++ b/sources/core/Stride.Core.Design.Tests/TypeConverters/TestMatrixConverter.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using System.Collections;
diff --git a/sources/core/Stride.Core.Design.Tests/Windows/TestAppHelper.cs b/sources/core/Stride.Core.Design.Tests/Windows/TestAppHelper.cs
index eda3f54e3f..6d30bd4d7d 100644
--- a/sources/core/Stride.Core.Design.Tests/Windows/TestAppHelper.cs
+++ b/sources/core/Stride.Core.Design.Tests/Windows/TestAppHelper.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using System.Text;
diff --git a/sources/core/Stride.Core.Mathematics.Tests/TestHalfVectors.cs b/sources/core/Stride.Core.Mathematics.Tests/TestHalfVectors.cs
index 36e386891d..86d8b34c3c 100644
--- a/sources/core/Stride.Core.Mathematics.Tests/TestHalfVectors.cs
+++ b/sources/core/Stride.Core.Mathematics.Tests/TestHalfVectors.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Xunit;
diff --git a/sources/core/Stride.Core.Tests/AnonymousDisposableTests.cs b/sources/core/Stride.Core.Tests/AnonymousDisposableTests.cs
new file mode 100644
index 0000000000..1e89d13df9
--- /dev/null
+++ b/sources/core/Stride.Core.Tests/AnonymousDisposableTests.cs
@@ -0,0 +1,60 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Xunit;
+
+namespace Stride.Core.Tests;
+
+public class AnonymousDisposableTests
+{
+ [Fact]
+ public void Constructor_WithNullAction_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => new AnonymousDisposable(null!));
+ }
+
+ [Fact]
+ public void Dispose_InvokesAction()
+ {
+ var disposed = false;
+ var disposable = new AnonymousDisposable(() => disposed = true);
+
+ disposable.Dispose();
+
+ Assert.True(disposed);
+ }
+
+ [Fact]
+ public void Dispose_CalledMultipleTimes_InvokesActionOnlyOnce()
+ {
+ var disposeCount = 0;
+ var disposable = new AnonymousDisposable(() => disposeCount++);
+
+ disposable.Dispose();
+ disposable.Dispose();
+ disposable.Dispose();
+
+ Assert.Equal(1, disposeCount);
+ }
+
+ [Fact]
+ public void Dispose_WithExceptionInAction_PropagatesException()
+ {
+ var disposable = new AnonymousDisposable(() => throw new InvalidOperationException("Test exception"));
+
+ Assert.Throws(() => disposable.Dispose());
+ }
+
+ [Fact]
+ public void Dispose_WithUsingStatement_InvokesAction()
+ {
+ var disposed = false;
+
+ using (new AnonymousDisposable(() => disposed = true))
+ {
+ Assert.False(disposed);
+ }
+
+ Assert.True(disposed);
+ }
+}
diff --git a/sources/core/Stride.Core.Tests/Collections/ConstrainedListTests.cs b/sources/core/Stride.Core.Tests/Collections/ConstrainedListTests.cs
new file mode 100644
index 0000000000..0cf47809f6
--- /dev/null
+++ b/sources/core/Stride.Core.Tests/Collections/ConstrainedListTests.cs
@@ -0,0 +1,219 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Xunit;
+using Stride.Core.Collections;
+
+namespace Stride.Core.Tests.Collections;
+
+public class ConstrainedListTests
+{
+ [Fact]
+ public void Constructor_WithoutParameters_CreatesEmptyList()
+ {
+ var list = new ConstrainedList();
+
+ Assert.Empty(list);
+ Assert.True(list.ThrowException);
+ }
+
+ [Fact]
+ public void Constructor_WithConstraint_SetsProperties()
+ {
+ var list = new ConstrainedList((l, item) => item > 0, false, "Must be positive");
+
+ Assert.NotNull(list.Constraint);
+ Assert.False(list.ThrowException);
+ }
+
+ [Fact]
+ public void Add_WithoutConstraint_AddsItem()
+ {
+ var list = new ConstrainedList();
+
+ list.Add(1);
+ list.Add(2);
+
+ Assert.Equal(2, list.Count);
+ Assert.Contains(1, list);
+ Assert.Contains(2, list);
+ }
+
+ [Fact]
+ public void Add_WithPassingConstraint_AddsItem()
+ {
+ var list = new ConstrainedList((l, item) => item > 0);
+
+ list.Add(5);
+ list.Add(10);
+
+ Assert.Equal(2, list.Count);
+ }
+
+ [Fact]
+ public void Add_WithFailingConstraintAndThrowFalse_DoesNotAddItem()
+ {
+ var list = new ConstrainedList((l, item) => item > 0, false);
+
+ list.Add(-5);
+
+ Assert.Empty(list);
+ }
+
+ [Fact]
+ public void Add_WithFailingConstraintAndThrowTrue_ThrowsException()
+ {
+ var list = new ConstrainedList((l, item) => item > 0, true, "Must be positive");
+
+ var ex = Assert.Throws(() => list.Add(-5));
+ Assert.Contains("Must be positive", ex.Message);
+ }
+
+ [Fact]
+ public void Insert_WithPassingConstraint_InsertsItem()
+ {
+ var list = new ConstrainedList((l, item) => item > 0);
+ list.Add(10);
+ list.Add(30);
+
+ list.Insert(1, 20);
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(20, list[1]);
+ }
+
+ [Fact]
+ public void Insert_WithFailingConstraint_DoesNotInsert()
+ {
+ var list = new ConstrainedList((l, item) => item > 0, false);
+ list.Add(10);
+
+ list.Insert(0, -5);
+
+ Assert.Single(list);
+ }
+
+ [Fact]
+ public void Indexer_Set_WithPassingConstraint_UpdatesValue()
+ {
+ var list = new ConstrainedList((l, item) => item > 0);
+ list.Add(10);
+
+ list[0] = 20;
+
+ Assert.Equal(20, list[0]);
+ }
+
+ [Fact]
+ public void Indexer_Set_WithFailingConstraint_DoesNotUpdate()
+ {
+ var list = new ConstrainedList((l, item) => item > 0, false);
+ list.Add(10);
+
+ list[0] = -5;
+
+ Assert.Equal(10, list[0]);
+ }
+
+ [Fact]
+ public void Remove_RemovesItem()
+ {
+ var list = new ConstrainedList { 1, 2, 3 };
+
+ var removed = list.Remove(2);
+
+ Assert.True(removed);
+ Assert.Equal(2, list.Count);
+ Assert.DoesNotContain(2, list);
+ }
+
+ [Fact]
+ public void RemoveAt_RemovesItemAtIndex()
+ {
+ var list = new ConstrainedList { "a", "b", "c" };
+
+ list.RemoveAt(1);
+
+ Assert.Equal(2, list.Count);
+ Assert.Equal("a", list[0]);
+ Assert.Equal("c", list[1]);
+ }
+
+ [Fact]
+ public void Clear_RemovesAllItems()
+ {
+ var list = new ConstrainedList { 1, 2, 3, 4 };
+
+ list.Clear();
+
+ Assert.Empty(list);
+ }
+
+ [Fact]
+ public void Contains_ReturnsTrueForExistingItem()
+ {
+ var list = new ConstrainedList { "a", "b", "c" };
+
+ Assert.Contains("b", list);
+ Assert.DoesNotContain("z", list);
+ }
+
+ [Fact]
+ public void IndexOf_ReturnsCorrectIndex()
+ {
+ var list = new ConstrainedList { 10, 20, 30 };
+
+ Assert.Equal(1, list.IndexOf(20));
+ Assert.Equal(-1, list.IndexOf(99));
+ }
+
+ [Fact]
+ public void CopyTo_CopiesItemsToArray()
+ {
+ var list = new ConstrainedList { 1, 2, 3 };
+ var array = new int[5];
+
+ list.CopyTo(array, 1);
+
+ Assert.Equal(0, array[0]);
+ Assert.Equal(1, array[1]);
+ Assert.Equal(2, array[2]);
+ Assert.Equal(3, array[3]);
+ Assert.Equal(0, array[4]);
+ }
+
+ [Fact]
+ public void GetEnumerator_IteratesAllItems()
+ {
+ var list = new ConstrainedList { 1, 2, 3 };
+ var sum = 0;
+
+ foreach (var item in list)
+ {
+ sum += item;
+ }
+
+ Assert.Equal(6, sum);
+ }
+
+ [Fact]
+ public void IsReadOnly_ReturnsFalse()
+ {
+ var list = new ConstrainedList();
+
+ Assert.False(list.IsReadOnly);
+ }
+
+ [Fact]
+ public void Constraint_CanAccessListInPredicate()
+ {
+ var list = new ConstrainedList((l, item) => l.Count < 3, false);
+
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+ list.Add(4); // This should fail constraint (count already 3)
+
+ Assert.Equal(3, list.Count);
+ }
+}
diff --git a/sources/core/Stride.Core.Tests/Collections/FastListStructTests.cs b/sources/core/Stride.Core.Tests/Collections/FastListStructTests.cs
new file mode 100644
index 0000000000..f228acef29
--- /dev/null
+++ b/sources/core/Stride.Core.Tests/Collections/FastListStructTests.cs
@@ -0,0 +1,355 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Xunit;
+using Stride.Core.Collections;
+
+namespace Stride.Core.Tests.Collections;
+
+public class FastListStructTests
+{
+ [Fact]
+ public void Constructor_WithCapacity_CreatesEmptyListWithCapacity()
+ {
+ var list = new FastListStruct(10);
+
+ Assert.Equal(0, list.Count);
+ Assert.True(list.Items.Length >= 10);
+ }
+
+ [Fact]
+ public void Constructor_WithZeroCapacity_CreatesEmptyList()
+ {
+ var list = new FastListStruct(0);
+
+ Assert.Equal(0, list.Count);
+ Assert.Empty(list.Items);
+ }
+
+ [Fact]
+ public void Constructor_WithArray_CopiesArrayElements()
+ {
+ var array = new[] { 1, 2, 3, 4, 5 };
+
+ var list = new FastListStruct(array);
+
+ Assert.Equal(5, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(5, list[4]);
+ }
+
+ [Fact]
+ public void Constructor_WithFastList_CopiesElements()
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ var fastList = new FastList { 10, 20, 30 };
+#pragma warning restore CS0618
+
+ var list = new FastListStruct(fastList);
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(10, list[0]);
+ Assert.Equal(30, list[2]);
+ }
+
+ [Fact]
+ public void Add_AddsItemToEnd()
+ {
+ var list = new FastListStruct(2);
+
+ list.Add(1);
+ list.Add(2);
+
+ Assert.Equal(2, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(2, list[1]);
+ }
+
+ [Fact]
+ public void Add_AutomaticallyExpandsCapacity()
+ {
+ var list = new FastListStruct(2);
+
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(3, list[2]);
+ }
+
+ [Fact]
+ public void AddRange_AddsMultipleItems()
+ {
+ var list1 = new FastListStruct(5);
+ list1.Add(1);
+ list1.Add(2);
+
+ var list2 = new FastListStruct(5);
+ list2.Add(3);
+ list2.Add(4);
+
+ list1.AddRange(list2);
+
+ Assert.Equal(4, list1.Count);
+ Assert.Equal(3, list1[2]);
+ Assert.Equal(4, list1[3]);
+ }
+
+ [Fact]
+ public void Insert_InsertsItemAtIndex()
+ {
+ var list = new FastListStruct(5);
+ list.Add(1);
+ list.Add(3);
+
+ list.Insert(1, 2);
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(2, list[1]);
+ Assert.Equal(3, list[2]);
+ }
+
+ [Fact]
+ public void Insert_AtBeginning_ShiftsElements()
+ {
+ var list = new FastListStruct(5);
+ list.Add(2);
+ list.Add(3);
+
+ list.Insert(0, 1);
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(2, list[1]);
+ }
+
+ [Fact]
+ public void Insert_ExpandsCapacityIfNeeded()
+ {
+ var list = new FastListStruct(2);
+ list.Add(1);
+ list.Add(3);
+
+ list.Insert(1, 2);
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(2, list[1]);
+ }
+
+ [Fact]
+ public void RemoveAt_RemovesItemAndShifts()
+ {
+ var list = new FastListStruct(5);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ list.RemoveAt(1);
+
+ Assert.Equal(2, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(3, list[1]);
+ }
+
+ [Fact]
+ public void Remove_RemovesFirstOccurrence()
+ {
+ var list = new FastListStruct(5);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ var result = list.Remove(2);
+
+ Assert.True(result);
+ Assert.Equal(2, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(3, list[1]);
+ }
+
+ [Fact]
+ public void Remove_ReturnsFalseIfNotFound()
+ {
+ var list = new FastListStruct(5);
+ list.Add(1);
+ list.Add(2);
+
+ var result = list.Remove(99);
+
+ Assert.False(result);
+ Assert.Equal(2, list.Count);
+ }
+
+ [Fact]
+ public void Clear_RemovesAllItems()
+ {
+ var list = new FastListStruct(5);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ list.Clear();
+
+ Assert.Equal(0, list.Count);
+ }
+
+ [Fact]
+ public void Contains_ReturnsTrueForExistingItem()
+ {
+ var list = new FastListStruct(5);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ Assert.True(list.Contains(2));
+ Assert.False(list.Contains(99));
+ }
+
+ [Fact]
+ public void IndexOf_ReturnsCorrectIndex()
+ {
+ var list = new FastListStruct(5);
+ list.Add(10);
+ list.Add(20);
+ list.Add(30);
+
+ Assert.Equal(1, list.IndexOf(20));
+ Assert.Equal(-1, list.IndexOf(99));
+ }
+
+ [Fact]
+ public void Indexer_GetsAndSetsValues()
+ {
+ var list = new FastListStruct(5);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ Assert.Equal(2, list[1]);
+
+ list[1] = 22;
+
+ Assert.Equal(22, list[1]);
+ }
+
+ [Fact]
+ public void SwapRemoveAt_RemovesItemBySwappingWithLast()
+ {
+ var list = new FastListStruct(5);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+ list.Add(4);
+
+ list.SwapRemoveAt(1);
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(4, list[1]); // Last item swapped here
+ Assert.Equal(3, list[2]);
+ }
+
+ [Fact]
+ public void SwapRemoveAt_OnLastItem_JustRemoves()
+ {
+ var list = new FastListStruct(5);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ list.SwapRemoveAt(2);
+
+ Assert.Equal(2, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(2, list[1]);
+ }
+
+ [Fact]
+ public void ToArray_ReturnsArrayWithCorrectElements()
+ {
+ var list = new FastListStruct(10);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ var array = list.ToArray();
+
+ Assert.Equal(3, array.Length);
+ Assert.Equal(1, array[0]);
+ Assert.Equal(3, array[2]);
+ }
+
+ [Fact]
+ public void EnsureCapacity_IncreasesCapacity()
+ {
+ var list = new FastListStruct(2);
+ list.Add(1);
+ list.Add(2);
+
+ list.EnsureCapacity(10);
+
+ Assert.True(list.Items.Length >= 10);
+ Assert.Equal(2, list.Count);
+ }
+
+ [Fact]
+ public void ImplicitConversion_FromFastList()
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ var fastList = new FastList { 1, 2, 3 };
+#pragma warning restore CS0618
+
+ FastListStruct list = fastList;
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(2, list[1]);
+ }
+
+ [Fact]
+ public void ImplicitConversion_FromArray()
+ {
+ var array = new[] { 1, 2, 3 };
+
+ FastListStruct list = array;
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(2, list[1]);
+ }
+
+ [Fact]
+ public void GetEnumerator_IteratesOverItems()
+ {
+ var list = new FastListStruct(5);
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ var sum = 0;
+ foreach (var item in list)
+ {
+ sum += item;
+ }
+
+ Assert.Equal(6, sum);
+ }
+
+ [Fact]
+ public void Enumerator_ImplementsIEnumerator()
+ {
+ var list = new FastListStruct(5);
+ list.Add("a");
+ list.Add("b");
+ list.Add("c");
+
+ var enumerator = list.GetEnumerator();
+ Assert.True(enumerator.MoveNext());
+ Assert.Equal("a", enumerator.Current);
+ Assert.True(enumerator.MoveNext());
+ Assert.Equal("b", enumerator.Current);
+ Assert.True(enumerator.MoveNext());
+ Assert.Equal("c", enumerator.Current);
+ Assert.False(enumerator.MoveNext());
+ }
+}
diff --git a/sources/core/Stride.Core.Tests/Collections/FastListTests.cs b/sources/core/Stride.Core.Tests/Collections/FastListTests.cs
new file mode 100644
index 0000000000..76ffd5001d
--- /dev/null
+++ b/sources/core/Stride.Core.Tests/Collections/FastListTests.cs
@@ -0,0 +1,219 @@
+using Stride.Core.Collections;
+using Xunit;
+
+namespace Stride.Core.Tests.Collections;
+
+#pragma warning disable CS0618 // Type or member is obsolete
+
+public class FastListTests
+{
+ [Fact]
+ public void Constructor_Default_CreatesEmptyList()
+ {
+ var list = new FastList();
+
+ Assert.Empty(list);
+ Assert.NotNull(list.Items);
+ }
+
+ [Fact]
+ public void Constructor_WithCapacity_CreatesListWithSpecifiedCapacity()
+ {
+ var list = new FastList(10);
+
+ Assert.Empty(list);
+ Assert.Equal(10, list.Capacity);
+ }
+
+ [Fact]
+ public void Constructor_WithCollection_CopiesItems()
+ {
+ var source = new List { 1, 2, 3, 4, 5 };
+
+ var list = new FastList(source);
+
+ Assert.Equal(5, list.Count);
+ Assert.Equal(source, list);
+ }
+
+ [Fact]
+ public void Add_AddsItemsToList()
+ {
+ var list = new FastList();
+
+ list.Add(1);
+ list.Add(2);
+ list.Add(3);
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(2, list[1]);
+ Assert.Equal(3, list[2]);
+ }
+
+ [Fact]
+ public void Add_AutomaticallyIncreasesCapacity()
+ {
+ var list = new FastList(2);
+
+ list.Add(1);
+ list.Add(2);
+ list.Add(3); // Should trigger capacity increase
+
+ Assert.Equal(3, list.Count);
+ Assert.True(list.Capacity >= 3);
+ }
+
+ [Fact]
+ public void Clear_RemovesAllItems()
+ {
+ var list = new FastList { 1, 2, 3 };
+
+ list.Clear();
+
+ Assert.Empty(list);
+ }
+
+ [Fact]
+ public void Contains_ReturnsTrueForExistingItem()
+ {
+ var list = new FastList { 1, 2, 3 };
+
+ Assert.Contains(2, list);
+ Assert.DoesNotContain(4, list);
+ }
+
+ [Fact]
+ public void IndexOf_ReturnsCorrectIndex()
+ {
+ var list = new FastList { 10, 20, 30 };
+
+ Assert.Equal(1, list.IndexOf(20));
+ Assert.Equal(-1, list.IndexOf(40));
+ }
+
+ [Fact]
+ public void Insert_InsertsItemAtSpecifiedIndex()
+ {
+ var list = new FastList { 1, 3 };
+
+ list.Insert(1, 2);
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(2, list[1]);
+ Assert.Equal(3, list[2]);
+ }
+
+ [Fact]
+ public void Remove_RemovesSpecifiedItem()
+ {
+ var list = new FastList { 1, 2, 3 };
+
+ var result = list.Remove(2);
+
+ Assert.True(result);
+ Assert.Equal(2, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(3, list[1]);
+ }
+
+ [Fact]
+ public void Remove_ReturnsFalseForNonExistingItem()
+ {
+ var list = new FastList { 1, 2, 3 };
+
+ var result = list.Remove(4);
+
+ Assert.False(result);
+ Assert.Equal(3, list.Count);
+ }
+
+ [Fact]
+ public void RemoveAt_RemovesItemAtSpecifiedIndex()
+ {
+ var list = new FastList { 1, 2, 3 };
+
+ list.RemoveAt(1);
+
+ Assert.Equal(2, list.Count);
+ Assert.Equal(1, list[0]);
+ Assert.Equal(3, list[1]);
+ }
+
+ [Fact]
+ public void Indexer_GetAndSet_WorkCorrectly()
+ {
+ var list = new FastList { 1, 2, 3 };
+
+ Assert.Equal(2, list[1]);
+
+ list[1] = 20;
+
+ Assert.Equal(20, list[1]);
+ }
+
+ [Fact]
+ public void CopyTo_CopiesItemsToArray()
+ {
+ var list = new FastList { 1, 2, 3 };
+ var array = new int[5];
+
+ list.CopyTo(array, 1);
+
+ Assert.Equal(0, array[0]);
+ Assert.Equal(1, array[1]);
+ Assert.Equal(2, array[2]);
+ Assert.Equal(3, array[3]);
+ Assert.Equal(0, array[4]);
+ }
+
+ [Fact]
+ public void Enumerator_IteratesThroughAllItems()
+ {
+ var list = new FastList { 1, 2, 3 };
+ var items = new List();
+
+ foreach (var item in list)
+ {
+ items.Add(item);
+ }
+
+ Assert.Equal(3, items.Count);
+ Assert.Equal(new[] { 1, 2, 3 }, items);
+ }
+
+ [Fact]
+ public void Capacity_CanBeIncreased()
+ {
+ var list = new FastList { 1, 2, 3 };
+
+ list.Capacity = 10;
+
+ Assert.Equal(10, list.Capacity);
+ Assert.Equal(3, list.Count);
+ }
+
+ [Fact]
+ public void Capacity_CanBeDecreased()
+ {
+ var list = new FastList(10) { 1, 2, 3 };
+
+ list.Capacity = 5;
+
+ Assert.Equal(5, list.Capacity);
+ Assert.Equal(3, list.Count);
+ }
+
+ [Fact]
+ public void EnsureCapacity_IncreasesCapacityWhenNeeded()
+ {
+ var list = new FastList(2);
+
+ list.EnsureCapacity(10);
+
+ Assert.True(list.Capacity >= 10);
+ }
+}
+
+#pragma warning restore CS0618 // Type or member is obsolete
diff --git a/sources/core/Stride.Core.Tests/Collections/FastTrackingCollectionTests.cs b/sources/core/Stride.Core.Tests/Collections/FastTrackingCollectionTests.cs
new file mode 100644
index 0000000000..d06d363a0a
--- /dev/null
+++ b/sources/core/Stride.Core.Tests/Collections/FastTrackingCollectionTests.cs
@@ -0,0 +1,186 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using System.Collections.Specialized;
+using Stride.Core.Collections;
+using Xunit;
+
+namespace Stride.Core.Tests.Collections;
+
+public class FastTrackingCollectionTests
+{
+ [Fact]
+ public void Constructor_CreatesEmptyCollection()
+ {
+ var collection = new FastTrackingCollection();
+ Assert.Empty(collection);
+ }
+
+ [Fact]
+ public void Add_RaisesCollectionChangedEvent()
+ {
+ var collection = new FastTrackingCollection();
+ var eventRaised = false;
+ FastTrackingCollectionChangedEventArgs capturedArgs = default;
+
+ FastTrackingCollection.FastEventHandler handler =
+ (object sender, ref FastTrackingCollectionChangedEventArgs e) =>
+ {
+ eventRaised = true;
+ capturedArgs = e;
+ };
+ collection.CollectionChanged += handler;
+
+ collection.Add(42);
+
+ Assert.True(eventRaised);
+ Assert.Equal(NotifyCollectionChangedAction.Add, capturedArgs.Action);
+ Assert.Equal(42, capturedArgs.Item);
+ Assert.Equal(0, capturedArgs.Index);
+ }
+
+ [Fact]
+ public void Remove_RaisesCollectionChangedEvent()
+ {
+ var collection = new FastTrackingCollection { "test" };
+ var eventRaised = false;
+ FastTrackingCollectionChangedEventArgs capturedArgs = default;
+
+ FastTrackingCollection.FastEventHandler handler =
+ (object sender, ref FastTrackingCollectionChangedEventArgs e) =>
+ {
+ eventRaised = true;
+ capturedArgs = e;
+ };
+ collection.CollectionChanged += handler;
+
+ collection.RemoveAt(0);
+
+ Assert.True(eventRaised);
+ Assert.Equal(NotifyCollectionChangedAction.Remove, capturedArgs.Action);
+ Assert.Equal("test", capturedArgs.Item);
+ Assert.Equal(0, capturedArgs.Index);
+ }
+
+ [Fact]
+ public void Clear_RaisesRemoveEventsForAllItems()
+ {
+ var collection = new FastTrackingCollection { 1, 2, 3 };
+ var removeCount = 0;
+ var removedItems = new List();
+
+ FastTrackingCollection.FastEventHandler handler =
+ (object sender, ref FastTrackingCollectionChangedEventArgs e) =>
+ {
+ if (e.Action == NotifyCollectionChangedAction.Remove)
+ {
+ removeCount++;
+ removedItems.Add((int)e.Item!);
+ }
+ };
+ collection.CollectionChanged += handler;
+
+ collection.Clear();
+
+ Assert.Equal(3, removeCount);
+ Assert.Equal(new[] { 3, 2, 1 }, removedItems); // Reverse order
+ Assert.Empty(collection);
+ }
+
+ [Fact]
+ public void SetItem_RaisesRemoveAndAddEvents()
+ {
+ var collection = new FastTrackingCollection { "old" };
+ var events = new List<(NotifyCollectionChangedAction, object?)>();
+
+ FastTrackingCollection.FastEventHandler handler =
+ (object sender, ref FastTrackingCollectionChangedEventArgs e) =>
+ {
+ events.Add((e.Action, e.Item));
+ };
+ collection.CollectionChanged += handler;
+
+ collection[0] = "new";
+
+ Assert.Equal(2, events.Count);
+ Assert.Equal(NotifyCollectionChangedAction.Remove, events[0].Item1);
+ Assert.Equal("old", events[0].Item2);
+ Assert.Equal(NotifyCollectionChangedAction.Add, events[1].Item1);
+ Assert.Equal("new", events[1].Item2);
+ }
+
+ [Fact]
+ public void Insert_RaisesAddEventAtCorrectIndex()
+ {
+ var collection = new FastTrackingCollection { 1, 3 };
+ var eventRaised = false;
+ FastTrackingCollectionChangedEventArgs capturedArgs = default;
+
+ FastTrackingCollection.FastEventHandler handler =
+ (object sender, ref FastTrackingCollectionChangedEventArgs e) =>
+ {
+ eventRaised = true;
+ capturedArgs = e;
+ };
+ collection.CollectionChanged += handler;
+
+ collection.Insert(1, 2);
+
+ Assert.True(eventRaised);
+ Assert.Equal(NotifyCollectionChangedAction.Add, capturedArgs.Action);
+ Assert.Equal(2, capturedArgs.Item);
+ Assert.Equal(1, capturedArgs.Index);
+ Assert.Equal(new[] { 1, 2, 3 }, collection);
+ }
+
+ [Fact]
+ public void MultipleHandlers_AllReceiveEvents()
+ {
+ var collection = new FastTrackingCollection();
+ var handler1Called = false;
+ var handler2Called = false;
+
+ FastTrackingCollection.FastEventHandler handler1 =
+ (object sender, ref FastTrackingCollectionChangedEventArgs e) => handler1Called = true;
+ FastTrackingCollection.FastEventHandler handler2 =
+ (object sender, ref FastTrackingCollectionChangedEventArgs e) => handler2Called = true;
+ collection.CollectionChanged += handler1;
+ collection.CollectionChanged += handler2;
+
+ collection.Add(1);
+
+ Assert.True(handler1Called);
+ Assert.True(handler2Called);
+ }
+
+ [Fact]
+ public void RemoveHandler_StopsReceivingEvents()
+ {
+ var collection = new FastTrackingCollection();
+ var eventCount = 0;
+
+ FastTrackingCollection.FastEventHandler handler =
+ (object sender, ref FastTrackingCollectionChangedEventArgs e) => eventCount++;
+
+ collection.CollectionChanged += handler;
+ collection.Add(1);
+ Assert.Equal(1, eventCount);
+
+ collection.CollectionChanged -= handler;
+ collection.Add(2);
+ Assert.Equal(1, eventCount); // Should not increment
+ }
+
+ [Fact]
+ public void NoHandlers_OperationsWorkNormally()
+ {
+ var collection = new FastTrackingCollection();
+
+ collection.Add(1);
+ collection.Add(2);
+ collection.RemoveAt(0);
+ collection.Clear();
+
+ Assert.Empty(collection);
+ }
+}
diff --git a/sources/core/Stride.Core.Tests/Collections/IndexingDictionaryTests.cs b/sources/core/Stride.Core.Tests/Collections/IndexingDictionaryTests.cs
new file mode 100644
index 0000000000..6833e21c7c
--- /dev/null
+++ b/sources/core/Stride.Core.Tests/Collections/IndexingDictionaryTests.cs
@@ -0,0 +1,267 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Stride.Core.Collections;
+using Xunit;
+
+namespace Stride.Core.Tests.Collections;
+
+public class IndexingDictionaryTests
+{
+ [Fact]
+ public void Constructor_CreatesEmptyDictionary()
+ {
+ var dict = new IndexingDictionary();
+ Assert.Empty(dict);
+ Assert.Equal(0, dict.Count);
+ }
+
+ [Fact]
+ public void Add_AddsItemAtSpecificIndex()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(5, "value5");
+
+ Assert.Equal(1, dict.Count);
+ Assert.Equal("value5", dict[5]);
+ }
+
+ [Fact]
+ public void Add_DuplicateKey_ThrowsException()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(1, "first");
+
+ Assert.Throws(() => dict.Add(1, "second"));
+ }
+
+ [Fact]
+ public void Indexer_Get_ReturnsCorrectValue()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(0, "zero");
+ dict.Add(10, "ten");
+
+ Assert.Equal("zero", dict[0]);
+ Assert.Equal("ten", dict[10]);
+ }
+
+ [Fact]
+ public void Indexer_Set_UpdatesValue()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(3, "old");
+ dict[3] = "new";
+
+ Assert.Equal("new", dict[3]);
+ Assert.Equal(1, dict.Count);
+ }
+
+ [Fact]
+ public void Indexer_Set_AddsNewValue()
+ {
+ var dict = new IndexingDictionary();
+ dict[5] = "value";
+
+ Assert.Equal("value", dict[5]);
+ Assert.Equal(1, dict.Count);
+ }
+
+ [Fact]
+ public void ContainsKey_ExistingKey_ReturnsTrue()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(7, "seven");
+
+ Assert.True(dict.ContainsKey(7));
+ }
+
+ [Fact]
+ public void ContainsKey_NonExistingKey_ReturnsFalse()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(3, "three");
+
+ Assert.False(dict.ContainsKey(5));
+ }
+
+ [Fact]
+ public void TryGetValue_ExistingKey_ReturnsTrueAndValue()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(4, "four");
+
+ var result = dict.TryGetValue(4, out var value);
+
+ Assert.True(result);
+ Assert.Equal("four", value);
+ }
+
+ [Fact]
+ public void TryGetValue_NonExistingKey_ReturnsFalse()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(2, "two");
+
+ var result = dict.TryGetValue(8, out var value);
+
+ Assert.False(result);
+ Assert.Null(value);
+ }
+
+ [Fact]
+ public void Remove_ExistingItem_RemovesAndReturnsTrue()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(3, "three");
+ dict.Add(4, "four");
+
+ var removed = dict.Remove(3);
+
+ Assert.True(removed);
+ Assert.Equal(1, dict.Count);
+ Assert.False(dict.ContainsKey(3));
+ }
+
+ [Fact]
+ public void Remove_NonExistingItem_ReturnsFalse()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(1, "one");
+
+ var removed = dict.Remove(5);
+
+ Assert.False(removed);
+ Assert.Equal(1, dict.Count);
+ }
+
+ [Fact]
+ public void Remove_ShrinksList()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(0, "zero");
+ dict.Add(1, "one");
+ dict.Add(2, "two");
+ dict.Add(3, "three");
+
+ dict.Remove(3);
+ dict.Remove(2);
+
+ Assert.Equal(2, dict.Count);
+ }
+
+ [Fact]
+ public void Clear_RemovesAllItems()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(1, "one");
+ dict.Add(2, "two");
+ dict.Add(3, "three");
+
+ dict.Clear();
+
+ Assert.Empty(dict);
+ Assert.Equal(0, dict.Count);
+ }
+
+ [Fact]
+ public void Keys_ReturnsAllKeys()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(0, "zero");
+ dict.Add(2, "two");
+ dict.Add(5, "five");
+
+ var keys = dict.Keys.OrderBy(k => k).ToList();
+
+ Assert.Equal(new[] { 0, 2, 5 }, keys);
+ }
+
+ [Fact]
+ public void Values_ReturnsAllValues()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(0, "zero");
+ dict.Add(2, "two");
+ dict.Add(5, "five");
+
+ var values = dict.Values.OrderBy(v => v).ToList();
+
+ Assert.Equal(new[] { "five", "two", "zero" }, values);
+ }
+
+ [Fact]
+ public void GetEnumerator_EnumeratesKeyValuePairs()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(1, "one");
+ dict.Add(3, "three");
+ dict.Add(5, "five");
+
+ var pairs = dict.OrderBy(kvp => kvp.Key).ToList();
+
+ Assert.Equal(3, pairs.Count);
+ Assert.Equal(new KeyValuePair(1, "one"), pairs[0]);
+ Assert.Equal(new KeyValuePair(3, "three"), pairs[1]);
+ Assert.Equal(new KeyValuePair(5, "five"), pairs[2]);
+ }
+
+ [Fact]
+ public void Contains_ExistingPair_ReturnsTrue()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(2, "two");
+
+ Assert.True(dict.Contains(new KeyValuePair(2, "two")));
+ }
+
+ [Fact]
+ public void Contains_WrongValue_ReturnsFalse()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(2, "two");
+
+ Assert.False(dict.Contains(new KeyValuePair(2, "other")));
+ }
+
+ [Fact]
+ public void CopyTo_CopiesAllItems()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(1, "one");
+ dict.Add(3, "three");
+
+ var array = new KeyValuePair[4];
+ dict.CopyTo(array, 1);
+
+ Assert.Equal(default, array[0]);
+ Assert.NotEqual(default, array[1]);
+ Assert.NotEqual(default, array[2]);
+ Assert.Equal(default, array[3]);
+ }
+
+ [Fact]
+ public void SafeGet_InvalidIndex_ReturnsNull()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(5, "five");
+
+ Assert.Null(dict.SafeGet(-1));
+ Assert.Null(dict.SafeGet(10));
+ }
+
+ [Fact]
+ public void SparseIndices_WorksCorrectly()
+ {
+ var dict = new IndexingDictionary();
+ dict.Add(0, "zero");
+ dict.Add(100, "hundred");
+ dict.Add(50, "fifty");
+
+ Assert.Equal(3, dict.Count);
+ Assert.Equal("zero", dict[0]);
+ Assert.Equal("fifty", dict[50]);
+ Assert.Equal("hundred", dict[100]);
+ Assert.Null(dict.SafeGet(25));
+ }
+}
diff --git a/sources/core/Stride.Core.Tests/Collections/KeyedSortedListTests.cs b/sources/core/Stride.Core.Tests/Collections/KeyedSortedListTests.cs
new file mode 100644
index 0000000000..6ad553dde2
--- /dev/null
+++ b/sources/core/Stride.Core.Tests/Collections/KeyedSortedListTests.cs
@@ -0,0 +1,311 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Xunit;
+using Stride.Core.Collections;
+
+namespace Stride.Core.Tests.Collections;
+
+// Test implementation of KeyedSortedList for testing purposes
+public class TestKeyedSortedList : KeyedSortedList
+{
+ protected override string GetKeyForItem(TestItem item)
+ {
+ return item.Key;
+ }
+}
+
+public class TestItem
+{
+ public string Key { get; set; } = string.Empty;
+ public int Value { get; set; }
+
+ public TestItem(string key, int value)
+ {
+ Key = key;
+ Value = value;
+ }
+}
+
+public class KeyedSortedListTests
+{
+ [Fact]
+ public void Constructor_CreatesEmptyList()
+ {
+ var list = new TestKeyedSortedList();
+
+ Assert.Empty(list);
+ }
+
+ [Fact]
+ public void Add_AddsItemInSortedOrder()
+ {
+ var list = new TestKeyedSortedList();
+
+ list.Add(new TestItem("b", 2));
+ list.Add(new TestItem("a", 1));
+ list.Add(new TestItem("c", 3));
+
+ Assert.Equal(3, list.Count);
+ Assert.Equal("a", list[0].Key);
+ Assert.Equal("b", list[1].Key);
+ Assert.Equal("c", list[2].Key);
+ }
+
+ [Fact]
+ public void Add_ThrowsOnDuplicateKey()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("a", 1));
+
+ Assert.Throws(() => list.Add(new TestItem("a", 2)));
+ }
+
+ [Fact]
+ public void ContainsKey_ReturnsTrueForExistingKey()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("a", 1));
+ list.Add(new TestItem("b", 2));
+
+ Assert.True(list.ContainsKey("a"));
+ Assert.True(list.ContainsKey("b"));
+ Assert.False(list.ContainsKey("z"));
+ }
+
+ [Fact]
+ public void Contains_ReturnsTrueForExistingItem()
+ {
+ var list = new TestKeyedSortedList();
+ var item = new TestItem("a", 1);
+ list.Add(item);
+
+ Assert.Contains(item, list);
+ Assert.DoesNotContain(new TestItem("z", 99), list);
+ }
+
+ [Fact]
+ public void IndexerByKey_GetsCorrectItem()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("a", 1));
+ list.Add(new TestItem("b", 2));
+
+ var item = list["a"];
+
+ Assert.Equal(1, item.Value);
+ }
+
+ [Fact]
+ public void IndexerByKey_ThrowsForNonExistentKey()
+ {
+ var list = new TestKeyedSortedList();
+
+ Assert.Throws(() => list["nonexistent"]);
+ }
+
+ [Fact]
+ public void IndexerByKey_SetUpdatesExistingItem()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("a", 1));
+
+ list["a"] = new TestItem("a", 100);
+
+ Assert.Equal(100, list["a"].Value);
+ }
+
+ [Fact]
+ public void IndexerByKey_SetAddsNewItemIfNotExists()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("a", 1));
+
+ list["b"] = new TestItem("b", 2);
+
+ Assert.Equal(2, list.Count);
+ Assert.Equal(2, list["b"].Value);
+ }
+
+ [Fact]
+ public void IndexerByIndex_GetsAndSetsItems()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("a", 1));
+ list.Add(new TestItem("b", 2));
+
+ Assert.Equal(1, list[0].Value);
+
+ list[0] = new TestItem("a", 10);
+
+ Assert.Equal(10, list[0].Value);
+ }
+
+ [Fact]
+ public void TryGetValue_ReturnsItemIfExists()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("a", 1));
+
+ var found = list.TryGetValue("a", out var item);
+
+ Assert.True(found);
+ Assert.NotNull(item);
+ Assert.Equal(1, item.Value);
+ }
+
+ [Fact]
+ public void TryGetValue_ReturnsFalseIfNotExists()
+ {
+ var list = new TestKeyedSortedList();
+
+ var found = list.TryGetValue("nonexistent", out var item);
+
+ Assert.False(found);
+ Assert.Null(item);
+ }
+
+ [Fact]
+ public void Remove_WithKey_RemovesItem()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("a", 1));
+ list.Add(new TestItem("b", 2));
+
+ var removed = list.Remove("a");
+
+ Assert.True(removed);
+ Assert.Single(list);
+ Assert.False(list.ContainsKey("a"));
+ }
+
+ [Fact]
+ public void Remove_WithKey_ReturnsFalseIfNotFound()
+ {
+ var list = new TestKeyedSortedList();
+
+ var removed = list.Remove("nonexistent");
+
+ Assert.False(removed);
+ }
+
+ [Fact]
+ public void Remove_WithItem_RemovesItem()
+ {
+ var list = new TestKeyedSortedList();
+ var item = new TestItem("a", 1);
+ list.Add(item);
+ list.Add(new TestItem("b", 2));
+
+ list.Remove(item);
+
+ Assert.Single(list);
+ Assert.False(list.ContainsKey("a"));
+ }
+
+ [Fact]
+ public void Clear_RemovesAllItems()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("a", 1));
+ list.Add(new TestItem("b", 2));
+ list.Add(new TestItem("c", 3));
+
+ list.Clear();
+
+ Assert.Empty(list);
+ }
+
+ [Fact]
+ public void IndexOf_ReturnsCorrectIndex()
+ {
+ var list = new TestKeyedSortedList();
+ var item = new TestItem("b", 2);
+ list.Add(new TestItem("a", 1));
+ list.Add(item);
+ list.Add(new TestItem("c", 3));
+
+ var index = list.IndexOf(item);
+
+ Assert.Equal(1, index);
+ }
+
+ [Fact]
+ public void Sort_ReordersItemsAfterKeyMutation()
+ {
+ var list = new TestKeyedSortedList();
+ var item = new TestItem("b", 2);
+ list.Add(new TestItem("a", 1));
+ list.Add(item);
+ list.Add(new TestItem("c", 3));
+
+ // Mutate the key (this is generally not recommended but Sort() exists for this case)
+ item.Key = "z";
+ list.Sort();
+
+ Assert.Equal("a", list[0].Key);
+ Assert.Equal("c", list[1].Key);
+ Assert.Equal("z", list[2].Key);
+ }
+
+ [Fact]
+ public void CopyTo_CopiesItemsToArray()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("a", 1));
+ list.Add(new TestItem("b", 2));
+ list.Add(new TestItem("c", 3));
+
+ var array = new TestItem[5];
+ ((System.Collections.Generic.ICollection)list).CopyTo(array, 1);
+
+ Assert.Null(array[0]);
+ Assert.Equal("a", array[1].Key);
+ Assert.Equal("b", array[2].Key);
+ Assert.Equal("c", array[3].Key);
+ Assert.Null(array[4]);
+ }
+
+ [Fact]
+ public void IsReadOnly_ReturnsFalse()
+ {
+ var list = new TestKeyedSortedList();
+
+ Assert.False(list.IsReadOnly);
+ }
+
+ [Fact]
+ public void GetEnumerator_IteratesInSortedOrder()
+ {
+ var list = new TestKeyedSortedList();
+ list.Add(new TestItem("c", 3));
+ list.Add(new TestItem("a", 1));
+ list.Add(new TestItem("b", 2));
+
+ var keys = new List();
+ foreach (var item in list)
+ {
+ keys.Add(item.Key);
+ }
+
+ Assert.Equal(new[] { "a", "b", "c" }, keys);
+ }
+
+ [Fact]
+ public void SyncRoot_ReturnsSelf()
+ {
+ var list = new TestKeyedSortedList();
+ var collection = (System.Collections.ICollection)list;
+
+ Assert.Same(list, collection.SyncRoot);
+ }
+
+ [Fact]
+ public void IsSynchronized_ReturnsFalse()
+ {
+ var list = new TestKeyedSortedList();
+ var collection = (System.Collections.ICollection)list;
+
+ Assert.False(collection.IsSynchronized);
+ }
+}
diff --git a/sources/core/Stride.Core.Tests/Collections/MultiValueSortedDictionaryTests.cs b/sources/core/Stride.Core.Tests/Collections/MultiValueSortedDictionaryTests.cs
new file mode 100644
index 0000000000..ca211a4177
--- /dev/null
+++ b/sources/core/Stride.Core.Tests/Collections/MultiValueSortedDictionaryTests.cs
@@ -0,0 +1,359 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Stride.Core.Collections;
+using Xunit;
+
+namespace Stride.Core.Tests.Collections;
+
+public class MultiValueSortedDictionaryTests
+{
+ [Fact]
+ public void Constructor_CreatesEmptyDictionary()
+ {
+ var dict = new MultiValueSortedDictionary();
+ Assert.Empty(dict);
+ Assert.Empty(dict);
+ }
+
+ [Fact]
+ public void Constructor_WithComparer_UsesComparer()
+ {
+ var dict = new MultiValueSortedDictionary(StringComparer.OrdinalIgnoreCase);
+ Assert.Equal(StringComparer.OrdinalIgnoreCase, dict.Comparer);
+ }
+
+ [Fact]
+ public void Add_SingleValue_AddsToDictionary()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+
+ Assert.Single(dict);
+ }
+
+ [Fact]
+ public void Add_MultipleValuesWithSameKey_AllAdded()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "first");
+ dict.Add(1, "second");
+ dict.Add(1, "third");
+
+ // Note: Count reflects all key-value pairs added, not distinct keys
+ Assert.Equal(3, dict.Count);
+ // Indexer returns first value only, not all values
+ Assert.Equal("first", dict[1]);
+ }
+
+ [Fact]
+ public void Add_MaintainsSortedKeyOrder()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(5, "five");
+ dict.Add(2, "two");
+ dict.Add(8, "eight");
+ dict.Add(1, "one");
+
+ var keys = dict.Keys.ToList();
+ Assert.Equal(new[] { 1, 2, 5, 8 }, keys);
+ }
+
+ [Fact]
+ public void Indexer_ReturnsValuesForKey()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "a");
+ dict.Add(1, "b");
+ dict.Add(2, "c");
+
+ // Indexer returns single TValue, not IEnumerable
+ // Use Keys to check distinct keys
+ Assert.Equal(2, dict.Keys.Count);
+ Assert.True(dict.ContainsKey(1));
+ Assert.True(dict.ContainsKey(2));
+ }
+
+ [Fact]
+ public void Indexer_NonExistentKey_ThrowsKeyNotFoundException()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+
+ Assert.Throws(() => dict[99]);
+ }
+
+ [Fact]
+ public void ContainsKey_ExistingKey_ReturnsTrue()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(5, "five");
+
+ Assert.True(dict.ContainsKey(5));
+ }
+
+ [Fact]
+ public void ContainsKey_NonExistingKey_ReturnsFalse()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(5, "five");
+
+ Assert.False(dict.ContainsKey(10));
+ }
+
+ [Fact]
+ public void ContainsValue_ExistingValue_ReturnsTrue()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+ dict.Add(2, "two");
+
+ Assert.True(dict.ContainsValue("one"));
+ }
+
+ [Fact]
+ public void ContainsValue_NonExistingValue_ReturnsFalse()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+
+ Assert.False(dict.ContainsValue("ten"));
+ }
+
+ [Fact]
+ public void Remove_RemovesAllValuesForKey()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "a");
+ dict.Add(1, "b");
+ dict.Add(2, "c");
+
+ var removed = dict.Remove(1);
+
+ Assert.True(removed);
+ Assert.Single(dict); // Only key 2 remains with 1 value
+ Assert.False(dict.ContainsKey(1));
+ }
+
+ [Fact]
+ public void Remove_NonExistentKey_ReturnsFalse()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+
+ var removed = dict.Remove(5);
+
+ Assert.False(removed);
+ Assert.Single(dict); // Still has 1 key-value pair
+ }
+
+ [Fact]
+ public void Clear_RemovesAllElements()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+ dict.Add(2, "two");
+ dict.Add(3, "three");
+
+ dict.Clear();
+
+ Assert.Empty(dict);
+ }
+
+ [Fact]
+ public void Keys_ReturnsDistinctKeysInOrder()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(3, "a");
+ dict.Add(1, "b");
+ dict.Add(1, "c");
+ dict.Add(2, "d");
+
+ var keys = dict.Keys.ToList();
+
+ Assert.Equal(new[] { 1, 2, 3 }, keys);
+ }
+
+ [Fact]
+ public void Values_ReturnsAllValues()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "a");
+ dict.Add(1, "b");
+ dict.Add(2, "c");
+
+ var values = dict.Values.ToList();
+
+ Assert.Equal(3, values.Count);
+ Assert.Contains("a", values);
+ Assert.Contains("b", values);
+ Assert.Contains("c", values);
+ }
+
+ [Fact]
+ public void TryGetValue_ExistingKey_ReturnsTrueWithValues()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "a");
+ dict.Add(1, "b");
+
+ var result = dict.TryGetValue(1, out IEnumerable values);
+
+ Assert.True(result);
+ Assert.NotNull(values);
+ Assert.Equal(2, values.Count());
+ }
+
+ [Fact]
+ public void TryGetValue_NonExistingKey_ReturnsFalse()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+
+ var result = dict.TryGetValue(99, out IEnumerable values);
+
+ Assert.False(result);
+ Assert.Null(values);
+ }
+
+ [Fact]
+ public void GetEnumerator_EnumeratesKeyValuePairs()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "a");
+ dict.Add(1, "b");
+ dict.Add(2, "c");
+
+ var pairs = dict.ToList();
+
+ Assert.Equal(3, pairs.Count);
+ Assert.All(pairs, pair => Assert.True(pair.Key == 1 || pair.Key == 2));
+ }
+
+ [Fact]
+ public void CaseInsensitiveComparer_MergesKeysIgnoringCase()
+ {
+ var dict = new MultiValueSortedDictionary(StringComparer.OrdinalIgnoreCase);
+ dict.Add("test", 1);
+ dict.Add("TEST", 2);
+ dict.Add("Test", 3);
+
+ Assert.Single(dict.Keys);
+ // All three values were added under the same key (case-insensitive)
+ Assert.Equal(3, dict.Count);
+ }
+
+ [Fact]
+ public void Count_ReflectsTotalKeyValuePairs()
+ {
+ var dict = new MultiValueSortedDictionary();
+
+ Assert.Empty(dict);
+
+ dict.Add(1, "a");
+ Assert.Single(dict);
+
+ dict.Add(1, "b");
+ Assert.Equal(2, dict.Count);
+
+ dict.Add(2, "c");
+ Assert.Equal(3, dict.Count);
+
+ dict.Remove(1);
+ Assert.Single(dict);
+ }
+
+ [Fact]
+ public void MixedOperations_MaintainsConsistency()
+ {
+ var dict = new MultiValueSortedDictionary();
+
+ dict.Add(5, "five-1");
+ dict.Add(3, "three");
+ dict.Add(5, "five-2");
+ dict.Add(1, "one");
+
+ Assert.Equal(4, dict.Count);
+ Assert.Equal(3, dict.Keys.Count);
+
+ dict.Remove(5);
+
+ Assert.Equal(2, dict.Count);
+ Assert.Equal(2, dict.Keys.Count);
+ Assert.Equal(new[] { 1, 3 }, dict.Keys);
+ }
+
+ [Fact]
+ public void AddKeyValuePair_Works()
+ {
+ var dict = new MultiValueSortedDictionary();
+ ((ICollection>)dict).Add(new KeyValuePair(1, "one"));
+
+ Assert.Single(dict);
+ // Indexer returns single value, not collection
+ Assert.Equal("one", dict[1]);
+ }
+
+ [Fact]
+ public void ContainsKeyValuePair_ExistingPair_ReturnsTrue()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+
+ var contains = ((ICollection>)dict).Contains(new KeyValuePair(1, "one"));
+
+ Assert.True(contains);
+ }
+
+ [Fact]
+ public void ContainsKeyValuePair_NonExistingPair_ReturnsFalse()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+
+ var contains = ((ICollection>)dict).Contains(new KeyValuePair(1, "two"));
+
+ Assert.False(contains);
+ }
+
+ [Fact]
+ public void RemoveKeyValuePair_ExistingPair_ReturnsTrue()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "a");
+ dict.Add(1, "b");
+
+ var removed = ((ICollection>)dict).Remove(new KeyValuePair(1, "a"));
+
+ Assert.True(removed);
+ Assert.Single(dict); // Still has key 1 with value "b"
+ Assert.Equal("b", dict[1]);
+ }
+
+ [Fact]
+ public void RemoveKeyValuePair_NonExistingPair_ReturnsFalse()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+
+ var removed = ((ICollection>)dict).Remove(new KeyValuePair(1, "two"));
+
+ Assert.False(removed);
+ Assert.Single(dict);
+ }
+
+ [Fact]
+ public void CopyTo_CopiesAllElements()
+ {
+ var dict = new MultiValueSortedDictionary();
+ dict.Add(1, "one");
+ dict.Add(2, "two");
+
+ var array = new KeyValuePair[3];
+ ((ICollection>)dict).CopyTo(array, 1);
+
+ Assert.Equal(default, array[0]);
+ Assert.NotEqual(default, array[1]);
+ Assert.NotEqual(default, array[2]);
+ }
+}
diff --git a/sources/core/Stride.Core.Tests/Collections/MultiValueSortedListTests.cs b/sources/core/Stride.Core.Tests/Collections/MultiValueSortedListTests.cs
new file mode 100644
index 0000000000..b200dce5c3
--- /dev/null
+++ b/sources/core/Stride.Core.Tests/Collections/MultiValueSortedListTests.cs
@@ -0,0 +1,253 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Stride.Core.Collections;
+using Xunit;
+
+namespace Stride.Core.Tests.Collections;
+
+public class MultiValueSortedListTests
+{
+ [Fact]
+ public void Constructor_CreatesEmptyList()
+ {
+ var list = new MultiValueSortedList();
+ Assert.Empty(list);
+ }
+
+ [Fact]
+ public void Add_SingleValue_AddsToList()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(1, "one"));
+
+ Assert.Single(list);
+ }
+
+ [Fact]
+ public void Add_MultipleValuesWithSameKey_AllAdded()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(1, "first"));
+ list.Add(new KeyValuePair(1, "second"));
+ list.Add(new KeyValuePair(1, "third"));
+
+ Assert.Equal(3, list.Count);
+ var values = list[1].ToList();
+ Assert.Equal(3, values.Count);
+ Assert.Contains("first", values);
+ Assert.Contains("second", values);
+ Assert.Contains("third", values);
+ }
+
+ [Fact]
+ public void Add_MaintainsSortedOrder()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(5, "five"));
+ list.Add(new KeyValuePair(2, "two"));
+ list.Add(new KeyValuePair(8, "eight"));
+ list.Add(new KeyValuePair(1, "one"));
+
+ var keys = list.Keys.ToList();
+ Assert.Equal(new[] { 1, 2, 5, 8 }, keys);
+ }
+
+ [Fact]
+ public void Indexer_ReturnsValuesForKey()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(1, "a"));
+ list.Add(new KeyValuePair(1, "b"));
+ list.Add(new KeyValuePair(2, "c"));
+
+ var valuesFor1 = list[1].ToList();
+ var valuesFor2 = list[2].ToList();
+
+ Assert.Equal(2, valuesFor1.Count);
+ Assert.Contains("a", valuesFor1);
+ Assert.Contains("b", valuesFor1);
+ Assert.Single(valuesFor2);
+ Assert.Contains("c", valuesFor2);
+ }
+
+ [Fact]
+ public void Indexer_NonExistentKey_ReturnsEmpty()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(1, "one"));
+
+ var values = list[99].ToList();
+
+ Assert.Empty(values);
+ }
+
+ [Fact]
+ public void Contains_ExistingKey_ReturnsTrue()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(5, "five"));
+
+ Assert.True(list.Contains(5));
+ }
+
+ [Fact]
+ public void Contains_NonExistingKey_ReturnsFalse()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(5, "five"));
+
+ Assert.False(list.Contains(10));
+ }
+
+ [Fact]
+ public void ContainsKey_ExistingKey_ReturnsTrue()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(3, "three"));
+
+ Assert.True(list.ContainsKey(3));
+ }
+
+ [Fact]
+ public void ContainsKey_NonExistingKey_ReturnsFalse()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(3, "three"));
+
+ Assert.False(list.ContainsKey(7));
+ }
+
+ [Fact]
+ public void Remove_RemovesAllValuesForKey()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(1, "a"));
+ list.Add(new KeyValuePair(1, "b"));
+ list.Add(new KeyValuePair(2, "c"));
+
+ var removed = list.Remove(1);
+
+ Assert.True(removed);
+ Assert.Single(list);
+ Assert.False(list.ContainsKey(1));
+ }
+
+ [Fact]
+ public void Remove_NonExistentKey_ReturnsFalse()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(1, "one"));
+
+ var removed = list.Remove(5);
+
+ Assert.False(removed);
+ Assert.Single(list);
+ }
+
+ [Fact]
+ public void Keys_ReturnsDistinctKeysInOrder()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(3, "a"));
+ list.Add(new KeyValuePair(1, "b"));
+ list.Add(new KeyValuePair(1, "c"));
+ list.Add(new KeyValuePair(2, "d"));
+
+ var keys = list.Keys.ToList();
+
+ Assert.Equal(new[] { 1, 2, 3 }, keys);
+ }
+
+ [Fact]
+ public void Values_ReturnsAllValues()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(1, "a"));
+ list.Add(new KeyValuePair(1, "b"));
+ list.Add(new KeyValuePair(2, "c"));
+
+ var values = list.Values.ToList();
+
+ Assert.Equal(3, values.Count);
+ Assert.Contains("a", values);
+ Assert.Contains("b", values);
+ Assert.Contains("c", values);
+ }
+
+ [Fact]
+ public void GetEnumerator_GroupsByKey()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair(1, "a"));
+ list.Add(new KeyValuePair(1, "b"));
+ list.Add(new KeyValuePair(2, "c"));
+
+ var groups = list.ToList>();
+
+ Assert.Equal(2, groups.Count);
+ Assert.Equal(1, groups[0].Key);
+ Assert.Equal(2, groups[0].Count());
+ Assert.Equal(2, groups[1].Key);
+ Assert.Single(groups[1]);
+ }
+
+ [Fact]
+ public void IEnumerable_KeyValuePair_EnumeratesAllPairs()
+ {
+ var list = new MultiValueSortedList();
+ list.Add(new KeyValuePair