// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Runtime.InteropServices; using CommunityToolkit.HighPerformance.UnitTests.Buffers.Internals; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CommunityToolkit.HighPerformance.UnitTests.Extensions; public partial class Test_ReadOnlySpanExtensions { /// /// Gets the list of counts to test the extension for /// private static ReadOnlySpan TestCounts => new[] { 0, 1, 7, 128, 255, 256, short.MaxValue, short.MaxValue + 1, 123_938, 1_678_922, 71_890_819 }; [TestMethod] public void Test_ReadOnlySpanExtensions_RandomCount8() { TestForType((byte)123, CreateRandomData); TestForType((sbyte)123, CreateRandomData); TestForType(true, CreateRandomData); } [TestMethod] public void Test_ReadOnlySpanExtensions_RandomCount16() { TestForType((ushort)4712, CreateRandomData); TestForType((short)4712, CreateRandomData); TestForType((char)4712, CreateRandomData); } [TestMethod] public void Test_ReadOnlySpanExtensions_RandomCount32() { TestForType((int)37438941, CreateRandomData); TestForType((uint)37438941, CreateRandomData); TestForType((float)Math.PI, CreateRandomData); } [TestMethod] public void Test_ReadOnlySpanExtensions_RandomCount64() { TestForType((long)47128480128401, CreateRandomData); TestForType((ulong)47128480128401, CreateRandomData); TestForType(Math.PI, CreateRandomData); } #if NET6_0_OR_GREATER [TestMethod] public void Test_ReadOnlySpanExtensions_RandomCountPtr() { TestForType(nint.MaxValue / 2, CreateRandomData); TestForType(nuint.MaxValue / 2, CreateRandomData); } #endif [TestMethod] public void Test_ReadOnlySpanExtensions_RandomCountManaged() { Int? value = new(37438941); // We can skip the most expensive test in this case, as we're not testing // a SIMD enabled path. The last test requires a very high memory usage which // sometimes causes the CI test runner to fail with an out of memory exception. // Since we don't need to double check overflows in the managed case, which is // just a classic linear loop with some optimizations, omitting this case is fine. foreach (int count in TestCounts.Slice(0, TestCounts.Length - 1)) { Random? random = new(count); Int[] data = new Int[count]; foreach (ref Int item in data.AsSpan()) { item = new Int(random.Next()); } // Fill at least 20% of the items with a matching value int minimum = count / 20; for (int i = 0; i < minimum; i++) { data[random.Next(0, count)] = value; } int result = data.Count(value); int expected = CountWithForeach(data, value); Assert.AreEqual(result, expected, $"Failed {typeof(Int)} test with count {count}: got {result} instead of {expected}"); } } // Dummy type to test the managed code path of the API private sealed class Int : IEquatable { private int Value { get; } public Int(int value) => Value = value; public bool Equals(Int? other) { if (other is null) { return false; } if (ReferenceEquals(this, other)) { return true; } return this.Value == other.Value; } public override bool Equals(object? obj) { return ReferenceEquals(this, obj) || (obj is Int other && Equals(other)); } public override int GetHashCode() { return this.Value; } } [TestMethod] public void Test_ReadOnlySpanExtensions_FilledCount8() { TestForType((byte)123, (count, _) => CreateFilledData(count, (byte)123)); TestForType((sbyte)123, (count, _) => CreateFilledData(count, (sbyte)123)); TestForType(true, (count, _) => CreateFilledData(count, true)); } [TestMethod] public void Test_ReadOnlySpanExtensions_FilledCount16() { TestForType((ushort)4712, (count, _) => CreateFilledData(count, (ushort)4712)); TestForType((short)4712, (count, _) => CreateFilledData(count, (short)4712)); TestForType((char)4712, (count, _) => CreateFilledData(count, (char)4712)); } [TestMethod] public void Test_ReadOnlySpanExtensions_FilledCount32() { TestForType((int)37438941, (count, _) => CreateFilledData(count, (int)37438941)); TestForType((uint)37438941, (count, _) => CreateFilledData(count, (uint)37438941)); } [TestMethod] public void Test_ReadOnlySpanExtensions_FilledCount64() { TestForType((long)47128480128401, (count, _) => CreateFilledData(count, (long)47128480128401)); TestForType((ulong)47128480128401, (count, _) => CreateFilledData(count, (ulong)47128480128401)); } #if NET6_0_OR_GREATER [TestMethod] public void Test_ReadOnlySpanExtensions_FilledCountPtr() { TestForType((nint)37438941, (count, _) => CreateFilledData(count, (nint)37438941)); TestForType((nuint)37438941, (count, _) => CreateFilledData(count, (nuint)37438941)); } #endif /// /// Performs a test for a specified type. /// /// The type to test. /// The target value to look for. /// The function to use to create random data. private static void TestForType(T value, Func> provider) where T : unmanaged, IEquatable { foreach (int count in TestCounts) { using UnmanagedSpanOwner data = provider(count, value); int result = data.GetSpan().Count(value); int expected = CountWithForeach(data.GetSpan(), value); Assert.AreEqual(result, expected, $"Failed {typeof(T)} test with count {count}: got {result} instead of {expected}"); } } /// /// Counts the number of occurrences of a given value into a target instance. /// /// The type of items to count. /// The input instance to read. /// The value to look for. /// The number of occurrences of in . private static int CountWithForeach(ReadOnlySpan span, T value) where T : IEquatable { int count = 0; foreach (T? item in span) { if (item.Equals(value)) { count++; } } return count; } /// /// Creates a random array filled with random data. /// /// The type of items to put in the array. /// The number of array items to create. /// The value to look for. /// An array of random elements. private static UnmanagedSpanOwner CreateRandomData(int count, T value) where T : unmanaged { Random? random = new(count); UnmanagedSpanOwner data = new(count); foreach (ref byte n in MemoryMarshal.AsBytes(data.GetSpan())) { n = (byte)random.Next(0, byte.MaxValue); } // Fill at least 20% of the items with a matching value int minimum = count / 20; Span span = data.GetSpan(); for (int i = 0; i < minimum; i++) { span[random.Next(0, count)] = value; } return data; } /// /// Creates a array filled with a given value. /// /// The type of items to put in the array. /// The number of array items to create. /// The value to use to populate the array. /// An array of elements. private static UnmanagedSpanOwner CreateFilledData(int count, T value) where T : unmanaged { UnmanagedSpanOwner data = new(count); data.GetSpan().Fill(value); return data; } }