.NET-Community-Toolkit/tests/CommunityToolkit.HighPerfor.../Extensions/Test_ReadOnlySpanExtensions...

260 lines
8.7 KiB
C#

// 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
{
/// <summary>
/// Gets the list of counts to test the extension for
/// </summary>
private static ReadOnlySpan<int> 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<Int>
{
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
/// <summary>
/// Performs a test for a specified type.
/// </summary>
/// <typeparam name="T">The type to test.</typeparam>
/// <param name="value">The target value to look for.</param>
/// <param name="provider">The function to use to create random data.</param>
private static void TestForType<T>(T value, Func<int, T, UnmanagedSpanOwner<T>> provider)
where T : unmanaged, IEquatable<T>
{
foreach (int count in TestCounts)
{
using UnmanagedSpanOwner<T> 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}");
}
}
/// <summary>
/// Counts the number of occurrences of a given value into a target <see cref="ReadOnlySpan{T}"/> instance.
/// </summary>
/// <typeparam name="T">The type of items to count.</typeparam>
/// <param name="span">The input <see cref="ReadOnlySpan{T}"/> instance to read.</param>
/// <param name="value">The value to look for.</param>
/// <returns>The number of occurrences of <paramref name="value"/> in <paramref name="span"/>.</returns>
private static int CountWithForeach<T>(ReadOnlySpan<T> span, T value)
where T : IEquatable<T>
{
int count = 0;
foreach (T? item in span)
{
if (item.Equals(value))
{
count++;
}
}
return count;
}
/// <summary>
/// Creates a random <typeparamref name="T"/> array filled with random data.
/// </summary>
/// <typeparam name="T">The type of items to put in the array.</typeparam>
/// <param name="count">The number of array items to create.</param>
/// <param name="value">The value to look for.</param>
/// <returns>An array of random <typeparamref name="T"/> elements.</returns>
private static UnmanagedSpanOwner<T> CreateRandomData<T>(int count, T value)
where T : unmanaged
{
Random? random = new(count);
UnmanagedSpanOwner<T> 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<T> span = data.GetSpan();
for (int i = 0; i < minimum; i++)
{
span[random.Next(0, count)] = value;
}
return data;
}
/// <summary>
/// Creates a <typeparamref name="T"/> array filled with a given value.
/// </summary>
/// <typeparam name="T">The type of items to put in the array.</typeparam>
/// <param name="count">The number of array items to create.</param>
/// <param name="value">The value to use to populate the array.</param>
/// <returns>An array of <typeparamref name="T"/> elements.</returns>
private static UnmanagedSpanOwner<T> CreateFilledData<T>(int count, T value)
where T : unmanaged
{
UnmanagedSpanOwner<T> data = new(count);
data.GetSpan().Fill(value);
return data;
}
}