| File: ApplicationModel\ResourceAnnotationCollectionTests.cs | Web Access |
| Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Aspire.Hosting.Tests.ApplicationModel; public class ResourceAnnotationCollectionTests { [Fact] public void Add_InsertsItem() { var collection = new ResourceAnnotationCollection(); var annotation = new TestAnnotation("test"); collection.Add(annotation); Assert.Single(collection); Assert.Same(annotation, collection[0]); } [Fact] public void Remove_RemovesItem() { var collection = new ResourceAnnotationCollection(); var annotation = new TestAnnotation("test"); collection.Add(annotation); var removed = collection.Remove(annotation); Assert.True(removed); Assert.Empty(collection); } [Fact] public void Clear_RemovesAllItems() { var collection = new ResourceAnnotationCollection(); collection.Add(new TestAnnotation("one")); collection.Add(new TestAnnotation("two")); collection.Add(new TestAnnotation("three")); collection.Clear(); Assert.Empty(collection); } [Fact] public void Indexer_SetsItem() { var collection = new ResourceAnnotationCollection(); var original = new TestAnnotation("original"); var replacement = new TestAnnotation("replacement"); collection.Add(original); collection[0] = replacement; Assert.Single(collection); Assert.Same(replacement, collection[0]); } [Fact] public void OfType_ReturnsMatchingAnnotations() { var collection = new ResourceAnnotationCollection(); var testAnnotation1 = new TestAnnotation("test1"); var testAnnotation2 = new TestAnnotation("test2"); var otherAnnotation = new OtherAnnotation(); collection.Add(testAnnotation1); collection.Add(otherAnnotation); collection.Add(testAnnotation2); var testAnnotations = collection.OfType<TestAnnotation>().ToArray(); Assert.Equal(2, testAnnotations.Length); Assert.Contains(testAnnotation1, testAnnotations); Assert.Contains(testAnnotation2, testAnnotations); } [Fact] public void Any_ReturnsTrueWhenMatchingAnnotationExists() { var collection = new ResourceAnnotationCollection(); collection.Add(new TestAnnotation("test")); Assert.True(collection.Any()); Assert.True(collection.OfType<TestAnnotation>().Any()); Assert.False(collection.OfType<OtherAnnotation>().Any()); } [Fact] public void ToArray_CreatesArrayCopy() { var collection = new ResourceAnnotationCollection(); collection.Add(new TestAnnotation("one")); collection.Add(new TestAnnotation("two")); var array = collection.ToArray(); Assert.Equal(2, array.Length); Assert.Equal(collection[0], array[0]); Assert.Equal(collection[1], array[1]); } [Fact] public void FirstOrDefault_ReturnsFirstMatchingAnnotation() { var collection = new ResourceAnnotationCollection(); var first = new TestAnnotation("first"); var second = new TestAnnotation("second"); collection.Add(first); collection.Add(second); var result = collection.OfType<TestAnnotation>().FirstOrDefault(); Assert.Same(first, result); } [Fact] public void Enumeration_ReturnsSnapshot_ModificationsDuringEnumerationDoNotThrow() { var collection = new ResourceAnnotationCollection(); for (var i = 0; i < 100; i++) { collection.Add(new TestAnnotation($"item{i}")); } var enumeratedItems = new List<IResourceAnnotation>(); // Start enumerating, then modify collection during enumeration. // This should NOT throw because we enumerate a snapshot. foreach (var item in collection) { enumeratedItems.Add(item); if (enumeratedItems.Count == 50) { // Modify collection during enumeration - should not throw collection.Add(new TestAnnotation("new-during-enumeration")); } } // The snapshot had 100 items when enumeration started Assert.Equal(100, enumeratedItems.Count); // The collection now has 101 items Assert.Equal(101, collection.Count); } [Fact] public void Enumeration_ViaLinq_ReturnsSnapshot_ModificationsDuringEnumerationDoNotThrow() { var collection = new ResourceAnnotationCollection(); for (var i = 0; i < 100; i++) { collection.Add(new TestAnnotation($"item{i}")); } var count = 0; // Use LINQ's OfType which goes through IEnumerable<T> interface dispatch foreach (var item in collection.OfType<TestAnnotation>()) { count++; if (count == 50) { collection.Add(new TestAnnotation("new-during-enumeration")); } } Assert.Equal(100, count); Assert.Equal(101, collection.Count); } [Fact] public async Task ConcurrentEnumeration_WhileAddingAndRemoving_DoesNotThrow() { var collection = new ResourceAnnotationCollection(); // Pre-populate with some items for (var i = 0; i < 100; i++) { collection.Add(new TestAnnotation($"initial{i}")); } using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var exceptions = new List<Exception>(); var addRemoveIterations = 0; var enumerationIterations = 0; // Task that continuously adds and removes items var modifierTask = Task.Run(async () => { var random = new Random(42); while (!cts.Token.IsCancellationRequested) { try { // Add a new item collection.Add(new TestAnnotation($"added{Interlocked.Increment(ref addRemoveIterations)}")); // Remove a random item if collection has items if (collection.Count > 10) { collection.RemoveAt(random.Next(Math.Min(10, collection.Count))); } await Task.Yield(); } catch (Exception ex) { lock (exceptions) { exceptions.Add(ex); } } } }); // Multiple tasks that continuously enumerate the collection var enumeratorTasks = Enumerable.Range(0, 4).Select(i => Task.Run(async () => { while (!cts.Token.IsCancellationRequested) { try { // Direct enumeration foreach (var item in collection) { GC.KeepAlive(item); } // LINQ enumeration via OfType (tests interface dispatch) GC.KeepAlive(collection.OfType<TestAnnotation>().Count()); // LINQ enumeration via Any GC.KeepAlive(collection.Any()); Interlocked.Increment(ref enumerationIterations); await Task.Yield(); } catch (Exception ex) { lock (exceptions) { exceptions.Add(ex); } } } })).ToArray(); // Run for a short duration to exercise concurrent access await Task.Delay(500); await cts.CancelAsync(); await modifierTask; await Task.WhenAll(enumeratorTasks); // Verify no exceptions occurred Assert.Empty(exceptions); // Verify we actually did some work Assert.True(addRemoveIterations > 0, "Should have performed add/remove operations"); Assert.True(enumerationIterations > 0, "Should have performed enumeration operations"); } [Fact] public void NonGenericEnumerable_GetEnumerator_ReturnsSnapshot() { var collection = new ResourceAnnotationCollection(); collection.Add(new TestAnnotation("test")); // Cast to non-generic IEnumerable to test that interface implementation var enumerable = (System.Collections.IEnumerable)collection; var enumerator = enumerable.GetEnumerator(); // Modify during enumeration collection.Add(new TestAnnotation("new")); var count = 0; while (enumerator.MoveNext()) { count++; } // Should have iterated snapshot with 1 item, not 2 Assert.Equal(1, count); Assert.Equal(2, collection.Count); } [Fact] public void GenericEnumerable_GetEnumerator_ReturnsSnapshot() { var collection = new ResourceAnnotationCollection(); collection.Add(new TestAnnotation("test")); // Cast to generic IEnumerable<T> to test that interface implementation var enumerable = (IEnumerable<IResourceAnnotation>)collection; var enumerator = enumerable.GetEnumerator(); // Modify during enumeration collection.Add(new TestAnnotation("new")); var count = 0; while (enumerator.MoveNext()) { count++; } // Should have iterated snapshot with 1 item, not 2 Assert.Equal(1, count); Assert.Equal(2, collection.Count); } private sealed class TestAnnotation(string name) : IResourceAnnotation { public string Name { get; } = name; public override string ToString() => Name; } private sealed class OtherAnnotation : IResourceAnnotation { } }