Adding tests for enumerables

This commit is contained in:
Zoe Roux 2021-05-30 19:04:51 +02:00
parent 6763b2c0d1
commit d6630f29ea
3 changed files with 163 additions and 40 deletions

View File

@ -18,13 +18,13 @@ namespace Kyoo
/// <param name="mapper">The function that will map each items</param> /// <param name="mapper">The function that will map each items</param>
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam> /// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam> /// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped or null if the input map was null.</returns> /// <returns>The list mapped.</returns>
/// <exception cref="ArgumentNullException">mapper can't be null</exception> /// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
public static IEnumerable<T2> Map<T, T2>([CanBeNull] this IEnumerable<T> self, public static IEnumerable<T2> Map<T, T2>([NotNull] this IEnumerable<T> self,
[NotNull] Func<T, int, T2> mapper) [NotNull] Func<T, int, T2> mapper)
{ {
if (self == null) if (self == null)
return null; throw new ArgumentNullException(nameof(self));
if (mapper == null) if (mapper == null)
throw new ArgumentNullException(nameof(mapper)); throw new ArgumentNullException(nameof(mapper));
@ -46,20 +46,22 @@ namespace Kyoo
/// A map where the mapping function is asynchronous. /// A map where the mapping function is asynchronous.
/// Note: <see cref="SelectAsync{T,T2}"/> might interest you. /// Note: <see cref="SelectAsync{T,T2}"/> might interest you.
/// </summary> /// </summary>
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param> /// <param name="self">The IEnumerable to map.</param>
/// <param name="mapper">The asynchronous function that will map each items</param> /// <param name="mapper">The asynchronous function that will map each items</param>
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam> /// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam> /// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped as an AsyncEnumerable</returns> /// <returns>The list mapped as an AsyncEnumerable</returns>
/// <exception cref="ArgumentNullException">mapper can't be null</exception> /// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
public static async IAsyncEnumerable<T2> MapAsync<T, T2>([CanBeNull] this IEnumerable<T> self, public static IAsyncEnumerable<T2> MapAsync<T, T2>([NotNull] this IEnumerable<T> self,
[NotNull] Func<T, int, Task<T2>> mapper) [NotNull] Func<T, int, Task<T2>> mapper)
{ {
if (self == null) if (self == null)
yield break; throw new ArgumentNullException(nameof(self));
if (mapper == null) if (mapper == null)
throw new ArgumentNullException(nameof(mapper)); throw new ArgumentNullException(nameof(mapper));
static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, Task<T2>> mapper)
{
using IEnumerator<T> enumerator = self.GetEnumerator(); using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0; int index = 0;
@ -70,6 +72,9 @@ namespace Kyoo
} }
} }
return Generator(self, mapper);
}
/// <summary> /// <summary>
/// An asynchronous version of Select. /// An asynchronous version of Select.
/// </summary> /// </summary>
@ -78,21 +83,26 @@ namespace Kyoo
/// <typeparam name="T">The type of items in <see cref="self"/></typeparam> /// <typeparam name="T">The type of items in <see cref="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam> /// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped as an AsyncEnumerable</returns> /// <returns>The list mapped as an AsyncEnumerable</returns>
/// <exception cref="ArgumentNullException">mapper can't be null</exception> /// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
public static async IAsyncEnumerable<T2> SelectAsync<T, T2>([CanBeNull] this IEnumerable<T> self, public static IAsyncEnumerable<T2> SelectAsync<T, T2>([NotNull] this IEnumerable<T> self,
[NotNull] Func<T, Task<T2>> mapper) [NotNull] Func<T, Task<T2>> mapper)
{ {
if (self == null) if (self == null)
yield break; throw new ArgumentNullException(nameof(self));
if (mapper == null) if (mapper == null)
throw new ArgumentNullException(nameof(mapper)); throw new ArgumentNullException(nameof(mapper));
static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, Task<T2>> mapper)
{
using IEnumerator<T> enumerator = self.GetEnumerator(); using IEnumerator<T> enumerator = self.GetEnumerator();
while (enumerator.MoveNext()) while (enumerator.MoveNext())
yield return await mapper(enumerator.Current); yield return await mapper(enumerator.Current);
} }
return Generator(self, mapper);
}
/// <summary> /// <summary>
/// Convert an AsyncEnumerable to a List by waiting for every item. /// Convert an AsyncEnumerable to a List by waiting for every item.
/// </summary> /// </summary>
@ -100,26 +110,38 @@ namespace Kyoo
/// <typeparam name="T">The type of items in the async list and in the returned list.</typeparam> /// <typeparam name="T">The type of items in the async list and in the returned list.</typeparam>
/// <returns>A task that will return a simple list</returns> /// <returns>A task that will return a simple list</returns>
/// <exception cref="ArgumentNullException">The list can't be null</exception> /// <exception cref="ArgumentNullException">The list can't be null</exception>
public static async Task<List<T>> ToListAsync<T>([NotNull] this IAsyncEnumerable<T> self) public static Task<List<T>> ToListAsync<T>([NotNull] this IAsyncEnumerable<T> self)
{ {
if (self == null) if (self == null)
throw new ArgumentNullException(nameof(self)); throw new ArgumentNullException(nameof(self));
static async Task<List<T>> ToList(IAsyncEnumerable<T> self)
{
List<T> ret = new(); List<T> ret = new();
await foreach (T i in self) await foreach (T i in self)
ret.Add(i); ret.Add(i);
return ret; return ret;
} }
return ToList(self);
}
/// <summary> /// <summary>
/// If the enumerable is empty, execute an action. /// If the enumerable is empty, execute an action.
/// </summary> /// </summary>
/// <param name="self">The enumerable to check</param> /// <param name="self">The enumerable to check</param>
/// <param name="action">The action to execute is the list is empty</param> /// <param name="action">The action to execute is the list is empty</param>
/// <typeparam name="T">The type of items inside the list</typeparam> /// <typeparam name="T">The type of items inside the list</typeparam>
/// <returns></returns> /// <exception cref="ArgumentNullException">The iterable and the action can't be null.</exception>
public static IEnumerable<T> IfEmpty<T>(this IEnumerable<T> self, Action action) /// <returns>The iterator proxied, there is no dual iterations.</returns>
public static IEnumerable<T> IfEmpty<T>([NotNull] this IEnumerable<T> self, [NotNull] Action action)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
if (action == null)
throw new ArgumentNullException(nameof(action));
static IEnumerable<T> Generator(IEnumerable<T> self, Action action)
{ {
using IEnumerator<T> enumerator = self.GetEnumerator(); using IEnumerator<T> enumerator = self.GetEnumerator();
@ -136,6 +158,9 @@ namespace Kyoo
while (enumerator.MoveNext()); while (enumerator.MoveNext());
} }
return Generator(self, action);
}
/// <summary> /// <summary>
/// A foreach used as a function with a little specificity: the list can be null. /// A foreach used as a function with a little specificity: the list can be null.
/// </summary> /// </summary>

View File

@ -1,14 +1,41 @@
using System.Reflection; using System.Reflection;
using Xunit; using Xunit;
using Xunit.Sdk;
namespace Kyoo.Tests namespace Kyoo.Tests
{ {
/// <summary>
/// Custom assertions used by Kyoo's tests.
/// </summary>
public static class KAssert public static class KAssert
{ {
/// <summary>
/// Check if every property of the item is equal to the other's object.
/// </summary>
/// <param name="expected">The value to check against</param>
/// <param name="value">The value to check</param>
/// <typeparam name="T">The type to check</typeparam>
public static void DeepEqual<T>(T expected, T value) public static void DeepEqual<T>(T expected, T value)
{ {
foreach (PropertyInfo property in typeof(T).GetProperties()) foreach (PropertyInfo property in typeof(T).GetProperties())
Assert.Equal(property.GetValue(expected), property.GetValue(value)); Assert.Equal(property.GetValue(expected), property.GetValue(value));
} }
/// <summary>
/// Explicitly fail a test.
/// </summary>
public static void Fail()
{
throw new XunitException();
}
/// <summary>
/// Explicitly fail a test.
/// </summary>
/// <param name="message">The message that will be seen in the test report</param>
public static void Fail(string message)
{
throw new XunitException(message);
}
} }
} }

View File

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Kyoo.Tests
{
public class EnumerableTests
{
[Fact]
public void MapTest()
{
int[] list = {1, 2, 3, 4};
Assert.All(list.Map((x, i) => (x, i)), x => Assert.Equal(x.x - 1, x.i));
Assert.Throws<ArgumentNullException>(() => list.Map(((Func<int, int, int>)null)!));
list = null;
Assert.Throws<ArgumentNullException>(() => list!.Map((x, _) => x + 1));
}
[Fact]
public async Task MapAsyncTest()
{
int[] list = {1, 2, 3, 4};
await foreach((int x, int i) in list.MapAsync((x, i) => Task.FromResult((x, i))))
{
Assert.Equal(x - 1, i);
}
Assert.Throws<ArgumentNullException>(() => list.MapAsync(((Func<int, int, Task<int>>)null)!));
list = null;
Assert.Throws<ArgumentNullException>(() => list!.MapAsync((x, _) => Task.FromResult(x + 1)));
}
[Fact]
public async Task SelectAsyncTest()
{
int[] list = {1, 2, 3, 4};
int i = 2;
await foreach(int x in list.SelectAsync(x => Task.FromResult(x + 1)))
{
Assert.Equal(i++, x);
}
Assert.Throws<ArgumentNullException>(() => list.SelectAsync(((Func<int, Task<int>>)null)!));
list = null;
Assert.Throws<ArgumentNullException>(() => list!.SelectAsync(x => Task.FromResult(x + 1)));
}
[Fact]
public async Task ToListAsyncTest()
{
int[] expected = {1, 2, 3, 4};
IAsyncEnumerable<int> list = expected.SelectAsync(Task.FromResult);
Assert.Equal(expected, await list.ToListAsync());
list = null;
await Assert.ThrowsAsync<ArgumentNullException>(() => list!.ToListAsync());
}
[Fact]
public void IfEmptyTest()
{
int[] list = {1, 2, 3, 4};
list = list.IfEmpty(() => KAssert.Fail("Empty action should not be triggered.")).ToArray();
Assert.Throws<ArgumentNullException>(() => list.IfEmpty(null!).ToList());
list = null;
Assert.Throws<ArgumentNullException>(() => list!.IfEmpty(() => {}).ToList());
list = Array.Empty<int>();
Assert.Throws<ArgumentException>(() => list.IfEmpty(() => throw new ArgumentException()).ToList());
Assert.Empty(list.IfEmpty(() => {}));
}
}
}