// 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;
}
}