From cd260c903978d6ff91c3c5982afb3b3db83321bd Mon Sep 17 00:00:00 2001 From: Matt Edmondson Date: Fri, 14 Aug 2026 05:26:06 +1000 Subject: [PATCH 1/3] [patch] Fix SonarCloud MSTest issues and unblock the build Build fixes (ktsu.Sdk 2.26.1 analyzers, were failing the build): - KTSU0007: Polyfill PackageReference now sets PrivateAssets="all" so the build-time-only package stops leaking into consumers dependency graphs. - KTSU0002: add Containers/AssemblyInfo.cs exposing internals to ktsu.Containers.Test. SonarCloud fixes (46 issues): - MSTEST0068 (34): CollectionAssert.AreEqual -> Assert.AreSequenceEqual. AreSequenceEqual takes IEnumerable, so the accompanying .ToArray() calls became redundant (IDE0305) and were dropped. - MSTEST0037 (11): Assert.AreEqual(n, x.Length) -> Assert.HasCount(n, x) for the span assertions, and Assert.IsTrue(a >= b) -> Assert.IsGreaterThanOrEqualTo(b, a) in SpscRingBufferTests. - MSTEST0049 (1): pass TestContext.CancellationToken to the Task.Run calls in ConcurrentProducerConsumer_TransfersAllItemsInOrder. Also refreshed the outdated CollectionAssert guidance in CLAUDE.md. Build clean, 351/351 tests pass. --- CLAUDE.md | 3 ++- Containers.Test/ContiguousCollectionTests.cs | 8 +++--- Containers.Test/ContiguousMapTests.cs | 8 +++--- Containers.Test/ContiguousSetTests.cs | 4 +-- .../InsertionOrderCollectionTests.cs | 2 +- Containers.Test/InsertionOrderMapTests.cs | 12 ++++----- Containers.Test/InsertionOrderSetTests.cs | 2 +- Containers.Test/OrderedCollectionTests.cs | 2 +- Containers.Test/OrderedMapTests.cs | 22 ++++++++-------- Containers.Test/OrderedSetTests.cs | 26 +++++++++---------- Containers.Test/SpscRingBufferTests.cs | 13 +++++++--- Containers/AssemblyInfo.cs | 3 +++ Containers/Containers.csproj | 2 +- 13 files changed, 58 insertions(+), 49 deletions(-) create mode 100644 Containers/AssemblyInfo.cs diff --git a/CLAUDE.md b/CLAUDE.md index bbc2117..2c18592 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,7 +69,8 @@ dotnet run --project Containers.Benchmarks --configuration Release -- --filter " ## Testing Conventions - Use MSTest framework (`[TestClass]`, `[TestMethod]`) -- Use `CollectionAssert.AreEqual` for collection equality (use `.ToArray()` if type inference issues arise) +- Use `Assert.AreSequenceEqual` for collection equality; `CollectionAssert.AreEqual` is flagged by MSTEST0068. It accepts any `IEnumerable`, so no `.ToArray()` is needed +- Use `Assert.HasCount` rather than asserting `.Count`/`.Length` via `Assert.AreEqual` (MSTEST0037) - Test edge cases: empty containers, boundary conditions, constructor validation ## Benchmarking Standards diff --git a/Containers.Test/ContiguousCollectionTests.cs b/Containers.Test/ContiguousCollectionTests.cs index 88bf135..c0d912c 100644 --- a/Containers.Test/ContiguousCollectionTests.cs +++ b/Containers.Test/ContiguousCollectionTests.cs @@ -351,7 +351,7 @@ public void GetEnumerator_IteratesInOrder() } // Assert - CollectionAssert.AreEqual(expected, enumerated); + Assert.AreSequenceEqual(expected, enumerated); } [TestMethod] @@ -364,7 +364,7 @@ public void AsSpan_ReturnsCorrectSpan() Span span = collection.AsSpan(); // Assert - Assert.AreEqual(4, span.Length); + Assert.HasCount(4, span); Assert.AreEqual(3, span[0]); Assert.AreEqual(1, span[1]); Assert.AreEqual(4, span[2]); @@ -381,7 +381,7 @@ public void AsReadOnlySpan_ReturnsCorrectSpan() ReadOnlySpan span = collection.AsReadOnlySpan(); // Assert - Assert.AreEqual(4, span.Length); + Assert.HasCount(4, span); Assert.AreEqual(3, span[0]); Assert.AreEqual(1, span[1]); Assert.AreEqual(4, span[2]); @@ -486,7 +486,7 @@ public void ContiguousMemoryLayout_OptimalForCachePerformance() // Test span access (only possible with contiguous memory) ReadOnlySpan span = collection.AsReadOnlySpan(); - Assert.AreEqual(1000, span.Length); + Assert.HasCount(1000, span); for (int i = 0; i < 1000; i++) { Assert.AreEqual(i, span[i]); diff --git a/Containers.Test/ContiguousMapTests.cs b/Containers.Test/ContiguousMapTests.cs index 0106bb3..a94fc89 100644 --- a/Containers.Test/ContiguousMapTests.cs +++ b/Containers.Test/ContiguousMapTests.cs @@ -411,7 +411,7 @@ public void GetKeysSpan_ReturnsCorrectSpan() ReadOnlySpan keysSpan = map.GetKeysSpan(); // Assert - Assert.AreEqual(4, keysSpan.Length); + Assert.HasCount(4, keysSpan); // Verify all keys are in the span HashSet spanKeys = []; @@ -440,7 +440,7 @@ public void GetValuesSpan_ReturnsCorrectSpan() ReadOnlySpan valuesSpan = map.GetValuesSpan(); // Assert - Assert.AreEqual(4, valuesSpan.Length); + Assert.HasCount(4, valuesSpan); // Verify all values are in the span HashSet spanValues = []; @@ -478,8 +478,8 @@ public void ContiguousMemoryLayout_OptimalForCachePerformance() // Test span access (only possible with contiguous memory) ReadOnlySpan keysSpan = map.GetKeysSpan(); ReadOnlySpan valuesSpan = map.GetValuesSpan(); - Assert.AreEqual(1000, keysSpan.Length); - Assert.AreEqual(1000, valuesSpan.Length); + Assert.HasCount(1000, keysSpan); + Assert.HasCount(1000, valuesSpan); // Verify all keys are in the span HashSet spanKeys = []; diff --git a/Containers.Test/ContiguousSetTests.cs b/Containers.Test/ContiguousSetTests.cs index 66030f9..321ff2f 100644 --- a/Containers.Test/ContiguousSetTests.cs +++ b/Containers.Test/ContiguousSetTests.cs @@ -227,7 +227,7 @@ public void AsReadOnlySpan_ReturnsCorrectSpan() ReadOnlySpan span = set.AsReadOnlySpan(); // Assert - Assert.AreEqual(4, span.Length); + Assert.HasCount(4, span); // Verify all elements are in the span HashSet spanElements = []; @@ -429,7 +429,7 @@ public void ContiguousMemoryLayout_OptimalForCachePerformance() // Test span access (only possible with contiguous memory) ReadOnlySpan span = set.AsReadOnlySpan(); - Assert.AreEqual(1000, span.Length); + Assert.HasCount(1000, span); // Verify all elements are in the span HashSet spanElements = []; diff --git a/Containers.Test/InsertionOrderCollectionTests.cs b/Containers.Test/InsertionOrderCollectionTests.cs index 4e0a138..ed44f64 100644 --- a/Containers.Test/InsertionOrderCollectionTests.cs +++ b/Containers.Test/InsertionOrderCollectionTests.cs @@ -342,7 +342,7 @@ public void GetEnumerator_IteratesInInsertionOrder() } // Assert - CollectionAssert.AreEqual(expected, enumerated); + Assert.AreSequenceEqual(expected, enumerated); } [TestMethod] diff --git a/Containers.Test/InsertionOrderMapTests.cs b/Containers.Test/InsertionOrderMapTests.cs index 5345b7c..9c92da1 100644 --- a/Containers.Test/InsertionOrderMapTests.cs +++ b/Containers.Test/InsertionOrderMapTests.cs @@ -52,7 +52,7 @@ public void Constructor_WithKeyValuePairs_AddsInInsertionOrder() // Verify insertion order int[] expectedOrder = [3, 1, 4, 2]; - CollectionAssert.AreEqual(expectedOrder, map.Keys.ToArray()); + Assert.AreSequenceEqual(expectedOrder, map.Keys); } [TestMethod] @@ -242,7 +242,7 @@ public void Remove_ExistingKey_RemovesAndReturnsTrue() // Verify order maintained int[] expectedOrder = [1, 3]; - CollectionAssert.AreEqual(expectedOrder, map.Keys.ToArray()); + Assert.AreSequenceEqual(expectedOrder, map.Keys); } [TestMethod] @@ -294,7 +294,7 @@ public void Keys_ReturnsKeysInInsertionOrder() // Assert int[] expectedOrder = [3, 1, 4, 2]; - CollectionAssert.AreEqual(expectedOrder, keys); + Assert.AreSequenceEqual(expectedOrder, keys); } [TestMethod] @@ -312,7 +312,7 @@ public void Values_ReturnsValuesInInsertionOrder() // Assert string[] expectedOrder = ["three", "one", "four", "two"]; - CollectionAssert.AreEqual(expectedOrder, values); + Assert.AreSequenceEqual(expectedOrder, values); } [TestMethod] @@ -406,7 +406,7 @@ public void MaintainsInsertionOrder_AfterMultipleOperations() // Assert int[] expectedOrder = [5, 2, 4]; // Insertion order minus removed items - CollectionAssert.AreEqual(expectedOrder, map.Keys.ToArray()); + Assert.AreSequenceEqual(expectedOrder, map.Keys); } [TestMethod] @@ -422,6 +422,6 @@ public void WorksWithCustomTypes() // Assert string[] expectedKeyOrder = ["charlie", "alpha", "bravo"]; - CollectionAssert.AreEqual(expectedKeyOrder, map.Keys.ToArray()); + Assert.AreSequenceEqual(expectedKeyOrder, map.Keys); } } diff --git a/Containers.Test/InsertionOrderSetTests.cs b/Containers.Test/InsertionOrderSetTests.cs index 25de19a..7e198bc 100644 --- a/Containers.Test/InsertionOrderSetTests.cs +++ b/Containers.Test/InsertionOrderSetTests.cs @@ -200,7 +200,7 @@ public void GetEnumerator_IteratesInInsertionOrder() } // Assert - CollectionAssert.AreEqual(expected, enumerated); // No duplicates, insertion order + Assert.AreSequenceEqual(expected, enumerated); // No duplicates, insertion order } [TestMethod] diff --git a/Containers.Test/OrderedCollectionTests.cs b/Containers.Test/OrderedCollectionTests.cs index 021bf11..e8cb56f 100644 --- a/Containers.Test/OrderedCollectionTests.cs +++ b/Containers.Test/OrderedCollectionTests.cs @@ -429,7 +429,7 @@ public void GetEnumerator_IteratesInSortedOrder() List actual = [.. collection]; // Assert - CollectionAssert.AreEqual(expected, actual); + Assert.AreSequenceEqual(expected, actual); } [TestMethod] diff --git a/Containers.Test/OrderedMapTests.cs b/Containers.Test/OrderedMapTests.cs index 76abff6..af0c180 100644 --- a/Containers.Test/OrderedMapTests.cs +++ b/Containers.Test/OrderedMapTests.cs @@ -37,7 +37,7 @@ public void Constructor_WithComparer_UsesCustomComparer() }; string[] expectedOrder = ["three", "two", "one"]; - CollectionAssert.AreEqual(expectedOrder, map.Values.ToArray()); + Assert.AreSequenceEqual(expectedOrder, map.Values); } /// @@ -68,8 +68,8 @@ public void Constructor_FromDictionary_CopiesAndSortsElements() Assert.AreEqual(3, map.Count); int[] expectedKeys = [1, 2, 3]; string[] expectedValues = ["one", "two", "three"]; - CollectionAssert.AreEqual(expectedKeys, map.Keys.ToArray()); - CollectionAssert.AreEqual(expectedValues, map.Values.ToArray()); + Assert.AreSequenceEqual(expectedKeys, map.Keys); + Assert.AreSequenceEqual(expectedValues, map.Values); } /// @@ -87,8 +87,8 @@ public void Add_MultipleElements_MaintainsSortedOrder() int[] expectedKeys = [1, 2, 3]; string[] expectedValues = ["one", "two", "three"]; - CollectionAssert.AreEqual(expectedKeys, map.Keys.ToArray()); - CollectionAssert.AreEqual(expectedValues, map.Values.ToArray()); + Assert.AreSequenceEqual(expectedKeys, map.Keys); + Assert.AreSequenceEqual(expectedValues, map.Values); } /// @@ -165,7 +165,7 @@ public void Indexer_SetNewKey_AddsKeyValuePair() Assert.AreEqual(3, map.Count); int[] expectedKeys = [1, 2, 3]; - CollectionAssert.AreEqual(expectedKeys, map.Keys.ToArray()); + Assert.AreSequenceEqual(expectedKeys, map.Keys); } /// @@ -237,7 +237,7 @@ public void Remove_ExistingKey_RemovesKeyAndReturnsTrue() Assert.AreEqual(2, map.Count); Assert.IsFalse(map.ContainsKey(2), "Removed key should no longer exist"); int[] expectedKeys = [1, 3]; - CollectionAssert.AreEqual(expectedKeys, map.Keys.ToArray()); + Assert.AreSequenceEqual(expectedKeys, map.Keys); } /// @@ -304,7 +304,7 @@ public void Keys_ReturnsSortedKeys() }; int[] expectedKeys = [1, 2, 3]; - CollectionAssert.AreEqual(expectedKeys, map.Keys.ToArray()); + Assert.AreSequenceEqual(expectedKeys, map.Keys); } /// @@ -321,7 +321,7 @@ public void Values_ReturnsValuesInKeyOrder() }; string[] expectedValues = ["one", "two", "three"]; - CollectionAssert.AreEqual(expectedValues, map.Values.ToArray()); + Assert.AreSequenceEqual(expectedValues, map.Values); } /// @@ -381,8 +381,8 @@ public void Clone_CreatesShallowCopy() OrderedMap clone = map.Clone(); Assert.AreEqual(map.Count, clone.Count); - CollectionAssert.AreEqual(map.Keys.ToArray(), clone.Keys.ToArray()); - CollectionAssert.AreEqual(map.Values.ToArray(), clone.Values.ToArray()); + Assert.AreSequenceEqual(map.Keys, clone.Keys); + Assert.AreSequenceEqual(map.Values, clone.Values); // Verify they are independent clone.Add(3, "three"); diff --git a/Containers.Test/OrderedSetTests.cs b/Containers.Test/OrderedSetTests.cs index 212853a..f43d5f0 100644 --- a/Containers.Test/OrderedSetTests.cs +++ b/Containers.Test/OrderedSetTests.cs @@ -48,7 +48,7 @@ public void Constructor_WithCollection_CreatesSetFromCollection() Assert.AreEqual(7, set.Count); // Duplicates removed int[] expected = [1, 2, 3, 4, 5, 6, 9]; - CollectionAssert.AreEqual(expected, set.ToArray()); + Assert.AreSequenceEqual(expected, set); } [TestMethod] @@ -59,7 +59,7 @@ public void Constructor_WithCollectionAndComparer_CreatesSetFromCollectionWithCo Assert.AreEqual(3, set.Count); // "apple" and "Apple" are treated as same string[] expected = ["apple", "BANANA", "cherry"]; - CollectionAssert.AreEqual(expected, set.ToArray()); + Assert.AreSequenceEqual(expected, set); } [TestMethod] @@ -111,7 +111,7 @@ public void Add_MultipleElements_MaintainsSortedOrder() Assert.AreEqual(4, set.Count); int[] expected = [1, 2, 5, 8]; - CollectionAssert.AreEqual(expected, set.ToArray()); + Assert.AreSequenceEqual(expected, set); } [TestMethod] @@ -176,7 +176,7 @@ public void CopyTo_ValidParameters_CopiesElements() set.CopyTo(array, 0); int[] expected = [1, 3, 4, 5]; // Note: only 4 unique elements - CollectionAssert.AreEqual(expected, array[0..4]); + Assert.AreSequenceEqual(expected, array[0..4]); } [TestMethod] @@ -213,7 +213,7 @@ public void Remove_ExistingElement_RemovesAndReturnsTrue() Assert.DoesNotContain(3, set); int[] expected = [1, 2, 4, 5]; - CollectionAssert.AreEqual(expected, set.ToArray()); + Assert.AreSequenceEqual(expected, set); } [TestMethod] @@ -277,7 +277,7 @@ public void GetEnumerator_Generic_IteratesInSortedOrder() } int[] expected = [1, 2, 5, 8, 9]; - CollectionAssert.AreEqual(expected, result); + Assert.AreSequenceEqual(expected, result); } [TestMethod] @@ -293,7 +293,7 @@ public void GetEnumerator_NonGeneric_IteratesInSortedOrder() } object[] expected = [1, 2, 5, 8, 9]; - CollectionAssert.AreEqual(expected, result); + Assert.AreSequenceEqual(expected, result); } [TestMethod] @@ -306,7 +306,7 @@ public void UnionWith_DisjointSets_AddsAllElements() Assert.AreEqual(6, set1.Count); int[] expected = [1, 2, 3, 4, 5, 6]; - CollectionAssert.AreEqual(expected, set1.ToArray()); + Assert.AreSequenceEqual(expected, set1); } [TestMethod] @@ -319,7 +319,7 @@ public void UnionWith_OverlappingSets_AddsUniqueElements() Assert.AreEqual(5, set1.Count); int[] expected = [1, 3, 4, 5, 6]; - CollectionAssert.AreEqual(expected, set1.ToArray()); + Assert.AreSequenceEqual(expected, set1); } [TestMethod] @@ -339,7 +339,7 @@ public void IntersectWith_OverlappingSets_KeepsCommonElements() Assert.AreEqual(3, set1.Count); int[] expected = [3, 4, 5]; - CollectionAssert.AreEqual(expected, set1.ToArray()); + Assert.AreSequenceEqual(expected, set1); } [TestMethod] @@ -370,7 +370,7 @@ public void ExceptWith_RemovesSpecifiedElements() Assert.AreEqual(3, set1.Count); int[] expected = [1, 3, 5]; - CollectionAssert.AreEqual(expected, set1.ToArray()); + Assert.AreSequenceEqual(expected, set1); } [TestMethod] @@ -390,7 +390,7 @@ public void SymmetricExceptWith_KeepsElementsInEitherButNotBoth() Assert.AreEqual(4, set1.Count); int[] expected = [1, 2, 5, 6]; - CollectionAssert.AreEqual(expected, set1.ToArray()); + Assert.AreSequenceEqual(expected, set1); } [TestMethod] @@ -575,7 +575,7 @@ public void Clone_CreatesIndependentCopy() OrderedSet clone = original.Clone(); // Verify they have the same content - CollectionAssert.AreEqual(original.ToArray(), clone.ToArray()); + Assert.AreSequenceEqual(original, clone); // Verify they are independent clone.Add(4); diff --git a/Containers.Test/SpscRingBufferTests.cs b/Containers.Test/SpscRingBufferTests.cs index 64da8e4..6595a12 100644 --- a/Containers.Test/SpscRingBufferTests.cs +++ b/Containers.Test/SpscRingBufferTests.cs @@ -9,6 +9,11 @@ namespace ktsu.Containers.Tests; [TestClass] public class SpscRingBufferTests { + /// + /// Gets or sets the test context, used to obtain the cancellation token for background tasks. + /// + public TestContext TestContext { get; set; } = null!; + [TestMethod] public void Constructor_NonPositiveCapacity_Throws() { @@ -20,7 +25,7 @@ public void Constructor_NonPositiveCapacity_Throws() public void Capacity_IsAtLeastRequested() { SpscRingBuffer buffer = new(5); - Assert.IsTrue(buffer.Capacity >= 5, "Usable capacity must be at least the requested amount."); + Assert.IsGreaterThanOrEqualTo(5, buffer.Capacity, "Usable capacity must be at least the requested amount."); } [TestMethod] @@ -60,7 +65,7 @@ public void TryEnqueue_WhenFull_ReturnsFalse() enqueued++; } - Assert.IsTrue(enqueued >= 4, "Should accept at least the requested capacity before reporting full."); + Assert.IsGreaterThanOrEqualTo(4, enqueued, "Should accept at least the requested capacity before reporting full."); Assert.IsFalse(buffer.TryEnqueue(999)); } @@ -114,7 +119,7 @@ public async Task ConcurrentProducerConsumer_TransfersAllItemsInOrder() Thread.SpinWait(1); } } - }); + }, TestContext.CancellationToken); Task consumer = Task.Run(() => { @@ -137,7 +142,7 @@ public async Task ConcurrentProducerConsumer_TransfersAllItemsInOrder() } return true; - }); + }, TestContext.CancellationToken); await Task.WhenAll(producer, consumer).ConfigureAwait(false); Assert.IsTrue(await consumer.ConfigureAwait(false), "All items must be received exactly once and in order."); diff --git a/Containers/AssemblyInfo.cs b/Containers/AssemblyInfo.cs new file mode 100644 index 0000000..83fb89e --- /dev/null +++ b/Containers/AssemblyInfo.cs @@ -0,0 +1,3 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ktsu.Containers.Test")] diff --git a/Containers/Containers.csproj b/Containers/Containers.csproj index 2c65b9b..706adb8 100644 --- a/Containers/Containers.csproj +++ b/Containers/Containers.csproj @@ -2,7 +2,7 @@ - + From 4b449ae0a1c72aee0531e11307212811dc1ae886 Mon Sep 17 00:00:00 2001 From: Matt Edmondson Date: Fri, 14 Aug 2026 05:36:34 +1000 Subject: [PATCH 2/3] [patch] Fix SonarCloud issues in library and benchmark code Library (9 issues): - S1939 (8): drop interfaces already implied by another interface in the base list. ContiguousCollection/InsertionOrderCollection/ OrderedCollection drop IReadOnlyCollection (implied by IReadOnlyList); RingBuffer drops IEnumerable and IReadOnlyCollection for the same reason. For the three Set types IReadOnlySet only exists on NET5_0_OR_GREATER, and the library still targets netstandard2.0/2.1, so IReadOnlyCollection moved into an #else branch rather than being removed outright - removing it would have dropped the interface from the lower targets. - S4136 (1): ContiguousMap.Entry - move Equals(Entry) next to Equals(object?) so the overloads are adjacent. Benchmarks (2 issues): - S4487 (1, HIGH): remove the unread keyValuePairs field in OrderedMapBenchmarks and its GlobalSetup assignment. - S1481 (1): discard the intentional indexer read in CacheUsagePatternRingBuffer instead of binding an unused local. Build clean on all 8 TFMs, 351/351 tests pass. --- .../CrossCollectionComparisonBenchmarks.cs | 2 +- Containers.Benchmarks/OrderedMapBenchmarks.cs | 2 -- Containers/ContiguousCollection.cs | 2 +- Containers/ContiguousMap.cs | 18 +++++++++--------- Containers/ContiguousSet.cs | 3 ++- Containers/InsertionOrderCollection.cs | 2 +- Containers/InsertionOrderSet.cs | 3 ++- Containers/OrderedCollection.cs | 2 +- Containers/OrderedSet.cs | 3 ++- Containers/RingBuffer.cs | 2 +- 10 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Containers.Benchmarks/CrossCollectionComparisonBenchmarks.cs b/Containers.Benchmarks/CrossCollectionComparisonBenchmarks.cs index c7d09da..154f528 100644 --- a/Containers.Benchmarks/CrossCollectionComparisonBenchmarks.cs +++ b/Containers.Benchmarks/CrossCollectionComparisonBenchmarks.cs @@ -462,7 +462,7 @@ public RingBuffer CacheUsagePatternRingBuffer() if (cache.Count > 10 && random.Next(5) == 0) { int index = random.Next(Math.Min(cache.Count, 10)); - int value = cache[index]; // Access recent item + _ = cache[index]; // Access recent item; result intentionally unused } } diff --git a/Containers.Benchmarks/OrderedMapBenchmarks.cs b/Containers.Benchmarks/OrderedMapBenchmarks.cs index 2726a1b..ccbe315 100644 --- a/Containers.Benchmarks/OrderedMapBenchmarks.cs +++ b/Containers.Benchmarks/OrderedMapBenchmarks.cs @@ -14,7 +14,6 @@ public class OrderedMapBenchmarks private readonly Random random = new(42); private int[] keys = []; private string[] values = []; - private KeyValuePair[] keyValuePairs = []; /// /// Gets or sets the number of elements to use in benchmarks. @@ -30,7 +29,6 @@ public void Setup() { keys = [.. Enumerable.Range(1, ElementCount).OrderBy(x => random.Next())]; values = [.. keys.Select(k => $"value{k}")]; - keyValuePairs = [.. keys.Zip(values, (k, v) => new KeyValuePair(k, v))]; } /// diff --git a/Containers/ContiguousCollection.cs b/Containers/ContiguousCollection.cs index 68d4742..9d0da4a 100644 --- a/Containers/ContiguousCollection.cs +++ b/Containers/ContiguousCollection.cs @@ -42,7 +42,7 @@ namespace ktsu.Containers; "CA1710:Identifiers should have correct suffix", Justification = "ContiguousCollection is a known collection name" )] -public class ContiguousCollection : ICollection, IReadOnlyCollection, IReadOnlyList +public class ContiguousCollection : ICollection, IReadOnlyList { /// /// The backing array that stores elements in contiguous memory. diff --git a/Containers/ContiguousMap.cs b/Containers/ContiguousMap.cs index 44059e7..b72142a 100644 --- a/Containers/ContiguousMap.cs +++ b/Containers/ContiguousMap.cs @@ -79,6 +79,15 @@ obj is Entry other && EqualityComparer.Default.Equals(Key, other.Key) && EqualityComparer.Default.Equals(Value, other.Value); + /// + /// Determines whether the current entry is equal to another entry. + /// + /// An entry to compare with this entry. + /// true if the current entry is equal to the other parameter; otherwise, false. + public bool Equals(Entry other) => + EqualityComparer.Default.Equals(Key, other.Key) + && EqualityComparer.Default.Equals(Value, other.Value); + /// /// Returns the hash code for this entry. /// @@ -95,15 +104,6 @@ public override int GetHashCode() #endif } - /// - /// Determines whether the current entry is equal to another entry. - /// - /// An entry to compare with this entry. - /// true if the current entry is equal to the other parameter; otherwise, false. - public bool Equals(Entry other) => - EqualityComparer.Default.Equals(Key, other.Key) - && EqualityComparer.Default.Equals(Value, other.Value); - /// /// Determines whether two entries are equal. /// diff --git a/Containers/ContiguousSet.cs b/Containers/ContiguousSet.cs index b4b76f9..bc60ff1 100644 --- a/Containers/ContiguousSet.cs +++ b/Containers/ContiguousSet.cs @@ -47,8 +47,9 @@ namespace ktsu.Containers; public class ContiguousSet : ISet #if NET5_0_OR_GREATER , IReadOnlySet -#endif +#else , IReadOnlyCollection +#endif { /// /// The backing array that stores elements in contiguous memory. diff --git a/Containers/InsertionOrderCollection.cs b/Containers/InsertionOrderCollection.cs index 26845a3..4e28f43 100644 --- a/Containers/InsertionOrderCollection.cs +++ b/Containers/InsertionOrderCollection.cs @@ -30,7 +30,7 @@ namespace ktsu.Containers; "CA1710:Identifiers should have correct suffix", Justification = "InsertionOrderCollection is a known collection name" )] -public class InsertionOrderCollection : ICollection, IReadOnlyCollection, IReadOnlyList +public class InsertionOrderCollection : ICollection, IReadOnlyList { /// /// The internal list that stores elements in insertion order. diff --git a/Containers/InsertionOrderSet.cs b/Containers/InsertionOrderSet.cs index 11112fe..9b92842 100644 --- a/Containers/InsertionOrderSet.cs +++ b/Containers/InsertionOrderSet.cs @@ -34,8 +34,9 @@ namespace ktsu.Containers; public class InsertionOrderSet : ISet #if NET5_0_OR_GREATER , IReadOnlySet -#endif +#else , IReadOnlyCollection +#endif { /// /// The internal list that stores elements in insertion order. diff --git a/Containers/OrderedCollection.cs b/Containers/OrderedCollection.cs index 43e16e7..01faf2d 100644 --- a/Containers/OrderedCollection.cs +++ b/Containers/OrderedCollection.cs @@ -32,7 +32,7 @@ namespace ktsu.Containers; "CA1710:Identifiers should have correct suffix", Justification = "OrderedCollection is a known collection name" )] -public class OrderedCollection : ICollection, IReadOnlyCollection, IReadOnlyList +public class OrderedCollection : ICollection, IReadOnlyList { /// /// The internal list that stores elements in sorted order. diff --git a/Containers/OrderedSet.cs b/Containers/OrderedSet.cs index 555e750..b02cd67 100644 --- a/Containers/OrderedSet.cs +++ b/Containers/OrderedSet.cs @@ -36,8 +36,9 @@ namespace ktsu.Containers; public class OrderedSet : ISet #if NET5_0_OR_GREATER , IReadOnlySet -#endif +#else , IReadOnlyCollection +#endif { /// /// The internal list that stores elements in sorted order. diff --git a/Containers/RingBuffer.cs b/Containers/RingBuffer.cs index 186b0dd..cc0497b 100644 --- a/Containers/RingBuffer.cs +++ b/Containers/RingBuffer.cs @@ -31,7 +31,7 @@ namespace ktsu.Containers; "CA1710:Identifiers should have correct suffix", Justification = "RingBuffer is a known collection name" )] -public class RingBuffer : IEnumerable, IReadOnlyCollection, IReadOnlyList +public class RingBuffer : IReadOnlyList { /// /// Gets or sets the internal buffer array. From 4dd9de16f03cae2e67f371e047e57ab29d172fd6 Mon Sep 17 00:00:00 2001 From: Matt Edmondson Date: Fri, 14 Aug 2026 05:49:17 +1000 Subject: [PATCH 3/3] [patch] Cover ContiguousMap.Entry equality and exclude benchmarks from Sonar The SonarCloud quality gate failed on new_coverage (0% vs 80% required), from exactly two uncovered new lines: - Containers/ContiguousMap.cs - ContiguousMap.Entry is a public struct with a full equality surface (Equals(Entry), Equals(object?), GetHashCode, == and !=) and had no tests at all. Added four tests covering equal/unequal entries, comparison against null and an unrelated type, and the Key/Value properties. - Containers.Benchmarks - benchmark code is never executed by the test suite, so it can only ever report 0% coverage. Set SonarQubeExclude on the benchmark project. Excluding the benchmark project also retires the 33 benchmark-only findings that are artifacts of benchmark style rather than defects: 29x S3267 (identical foreach/counter loops kept uniform across competing collections so the comparisons stay fair), 3x S4158 (operations on intentionally-empty collections in EmptyCollectionOperations, plus a foreach immediately after PushBack) and 1x S2583. Build clean on all 8 TFMs, 355/355 tests pass. --- .../Containers.Benchmarks.csproj | 9 ++++ Containers.Test/ContiguousMapTests.cs | 52 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/Containers.Benchmarks/Containers.Benchmarks.csproj b/Containers.Benchmarks/Containers.Benchmarks.csproj index dafc25f..68b7a11 100644 --- a/Containers.Benchmarks/Containers.Benchmarks.csproj +++ b/Containers.Benchmarks/Containers.Benchmarks.csproj @@ -6,6 +6,15 @@ enable Containers.Benchmarks ktsu.Containers.Benchmarks + + true diff --git a/Containers.Test/ContiguousMapTests.cs b/Containers.Test/ContiguousMapTests.cs index a94fc89..5a0659b 100644 --- a/Containers.Test/ContiguousMapTests.cs +++ b/Containers.Test/ContiguousMapTests.cs @@ -525,4 +525,56 @@ public void HandlesNullValues() Assert.IsTrue(map.TryGetValue(1, out string? value), "TryGetValue should return true for key with null value"); Assert.IsNull(value); } + + [TestMethod] + public void Entry_Equals_SameKeyAndValue_ReturnsTrue() + { + // Arrange + ContiguousMap.Entry left = new(1, "one"); + ContiguousMap.Entry right = new(1, "one"); + + // Act & Assert + Assert.IsTrue(left.Equals(right), "Entries with the same key and value should be equal"); + Assert.IsTrue(left.Equals((object)right), "Equals(object) should agree with Equals(Entry)"); + Assert.IsTrue(left == right, "operator == should report equality"); + Assert.IsFalse(left != right, "operator != should report equality"); + Assert.AreEqual(left.GetHashCode(), right.GetHashCode()); + } + + [TestMethod] + public void Entry_Equals_DifferentKeyOrValue_ReturnsFalse() + { + // Arrange + ContiguousMap.Entry entry = new(1, "one"); + ContiguousMap.Entry differentKey = new(2, "one"); + ContiguousMap.Entry differentValue = new(1, "two"); + + // Act & Assert + Assert.IsFalse(entry.Equals(differentKey), "Entries with different keys should not be equal"); + Assert.IsFalse(entry.Equals(differentValue), "Entries with different values should not be equal"); + Assert.IsTrue(entry != differentKey, "operator != should report inequality"); + Assert.IsFalse(entry == differentValue, "operator == should report inequality"); + } + + [TestMethod] + public void Entry_Equals_NonEntryObject_ReturnsFalse() + { + // Arrange + ContiguousMap.Entry entry = new(1, "one"); + + // Act & Assert + Assert.IsFalse(entry.Equals(null), "An entry should not equal null"); + Assert.IsFalse(entry.Equals("not an entry"), "An entry should not equal an unrelated type"); + } + + [TestMethod] + public void Entry_KeyAndValue_ExposeConstructorArguments() + { + // Arrange & Act + ContiguousMap.Entry entry = new(7, "seven"); + + // Assert + Assert.AreEqual(7, entry.Key); + Assert.AreEqual("seven", entry.Value); + } }