520 lines
16 KiB
C#
520 lines
16 KiB
C#
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
using FluentAssertions;
|
||
|
|
using SqrtSpace.SpaceTime.Linq;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace SqrtSpace.SpaceTime.Tests.Linq;
|
||
|
|
|
||
|
|
public class SpaceTimeEnumerableTests
|
||
|
|
{
|
||
|
|
private static IEnumerable<int> GenerateNumbers(int count)
|
||
|
|
{
|
||
|
|
for (int i = 0; i < count; i++)
|
||
|
|
{
|
||
|
|
yield return i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static IEnumerable<TestItem> GenerateTestItems(int count)
|
||
|
|
{
|
||
|
|
var random = new Random(42); // Fixed seed for reproducibility
|
||
|
|
for (int i = 0; i < count; i++)
|
||
|
|
{
|
||
|
|
yield return new TestItem
|
||
|
|
{
|
||
|
|
Id = i,
|
||
|
|
Value = random.Next(1000),
|
||
|
|
Category = $"Category{random.Next(10)}",
|
||
|
|
Date = DateTime.Today.AddDays(-random.Next(365))
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class TestItem
|
||
|
|
{
|
||
|
|
public int Id { get; set; }
|
||
|
|
public int Value { get; set; }
|
||
|
|
public string Category { get; set; } = "";
|
||
|
|
public DateTime Date { get; set; }
|
||
|
|
}
|
||
|
|
|
||
|
|
public class OrderByExternalTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public void OrderByExternal_SmallCollection_ReturnsSortedResults()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = new[] { 5, 2, 8, 1, 9, 3, 7, 4, 6 };
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.OrderByExternal(x => x).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void OrderByExternal_LargeCollection_ReturnsSortedResults()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(10_000).OrderBy(_ => Guid.NewGuid()).ToList();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.OrderByExternal(x => x).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().BeInAscendingOrder();
|
||
|
|
result.Should().HaveCount(10_000);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void OrderByExternal_WithCustomComparer_UsesComparer()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = new[] { "apple", "Banana", "cherry", "Date" };
|
||
|
|
var comparer = StringComparer.OrdinalIgnoreCase;
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.OrderByExternal(x => x, comparer).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().BeEquivalentTo(new[] { "apple", "Banana", "cherry", "Date" });
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void OrderByExternal_WithCustomBufferSize_RespectsBufferSize()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(1000).ToList();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.OrderByExternal(x => x, bufferSize: 10).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().BeInAscendingOrder();
|
||
|
|
result.Should().HaveCount(1000);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void OrderByDescendingExternal_ReturnsDescendingOrder()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = new[] { 5, 2, 8, 1, 9, 3, 7, 4, 6 };
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.OrderByDescendingExternal(x => x).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().BeEquivalentTo(new[] { 9, 8, 7, 6, 5, 4, 3, 2, 1 });
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void OrderByExternal_WithComplexKey_SortsCorrectly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateTestItems(100).ToList();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.OrderByExternal(x => x.Date)
|
||
|
|
.ThenByExternal(x => x.Value)
|
||
|
|
.ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().BeInAscendingOrder(x => x.Date)
|
||
|
|
.And.ThenBeInAscendingOrder(x => x.Value);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void OrderByExternal_EmptyCollection_ReturnsEmpty()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = Enumerable.Empty<int>();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.OrderByExternal(x => x).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().BeEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void OrderByExternal_NullKeySelector_ThrowsException()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = new[] { 1, 2, 3 };
|
||
|
|
|
||
|
|
// Act & Assert
|
||
|
|
var action = () => items.OrderByExternal<int, int>(null!).ToList();
|
||
|
|
action.Should().Throw<ArgumentNullException>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class GroupByExternalTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public void GroupByExternal_SimpleGrouping_ReturnsCorrectGroups()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.GroupByExternal(x => x % 3).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().HaveCount(3);
|
||
|
|
result.SelectMany(g => g).Should().BeEquivalentTo(items);
|
||
|
|
result.Single(g => g.Key == 0).Should().BeEquivalentTo(new[] { 3, 6, 9 });
|
||
|
|
result.Single(g => g.Key == 1).Should().BeEquivalentTo(new[] { 1, 4, 7, 10 });
|
||
|
|
result.Single(g => g.Key == 2).Should().BeEquivalentTo(new[] { 2, 5, 8 });
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void GroupByExternal_WithElementSelector_TransformsElements()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateTestItems(100).ToList();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.GroupByExternal(
|
||
|
|
x => x.Category,
|
||
|
|
x => x.Value
|
||
|
|
).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().HaveCount(10); // 10 categories
|
||
|
|
result.Sum(g => g.Count()).Should().Be(100);
|
||
|
|
result.All(g => g.All(v => v.GetType() == typeof(int))).Should().BeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void GroupByExternal_WithResultSelector_AppliesTransformation()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateTestItems(50).ToList();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.GroupByExternal(
|
||
|
|
x => x.Category,
|
||
|
|
x => x.Value,
|
||
|
|
(key, values) => new
|
||
|
|
{
|
||
|
|
Category = key,
|
||
|
|
Sum = values.Sum(),
|
||
|
|
Count = values.Count()
|
||
|
|
}
|
||
|
|
).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().HaveCount(10);
|
||
|
|
result.Sum(x => x.Count).Should().Be(50);
|
||
|
|
result.All(x => x.Sum > 0).Should().BeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void GroupByExternal_LargeDataset_HandlesCorrectly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(10_000).Select(x => new { Id = x, Group = x % 100 });
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.GroupByExternal(x => x.Group).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().HaveCount(100);
|
||
|
|
result.All(g => g.Count() == 100).Should().BeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void GroupByExternal_WithCustomComparer_UsesComparer()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = new[] { "apple", "Apple", "banana", "Banana", "cherry" };
|
||
|
|
var comparer = StringComparer.OrdinalIgnoreCase;
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.GroupByExternal(x => x, comparer).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().HaveCount(3);
|
||
|
|
result.Single(g => comparer.Equals(g.Key, "apple")).Count().Should().Be(2);
|
||
|
|
result.Single(g => comparer.Equals(g.Key, "banana")).Count().Should().Be(2);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void GroupByExternal_EmptyCollection_ReturnsEmpty()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = Enumerable.Empty<int>();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.GroupByExternal(x => x).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().BeEmpty();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class DistinctExternalTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public void DistinctExternal_RemovesDuplicates()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = new[] { 1, 2, 3, 2, 4, 3, 5, 1, 6, 4, 7 };
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.DistinctExternal().ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5, 6, 7 });
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void DistinctExternal_WithComparer_UsesComparer()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = new[] { "apple", "Apple", "banana", "Banana", "cherry" };
|
||
|
|
var comparer = StringComparer.OrdinalIgnoreCase;
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.DistinctExternal(comparer).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().HaveCount(3);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void DistinctExternal_LargeDataset_HandlesCorrectly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(10_000).Concat(GenerateNumbers(10_000));
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.DistinctExternal().ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().HaveCount(10_000);
|
||
|
|
result.Should().BeEquivalentTo(Enumerable.Range(0, 10_000));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void DistinctExternal_PreservesFirstOccurrence()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = new[]
|
||
|
|
{
|
||
|
|
new TestItem { Id = 1, Value = 100 },
|
||
|
|
new TestItem { Id = 2, Value = 200 },
|
||
|
|
new TestItem { Id = 1, Value = 300 },
|
||
|
|
new TestItem { Id = 3, Value = 400 }
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = items.DistinctExternal(new TestItemIdComparer()).ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().HaveCount(3);
|
||
|
|
result.Single(x => x.Id == 1).Value.Should().Be(100); // First occurrence
|
||
|
|
}
|
||
|
|
|
||
|
|
private class TestItemIdComparer : IEqualityComparer<TestItem>
|
||
|
|
{
|
||
|
|
public bool Equals(TestItem? x, TestItem? y)
|
||
|
|
{
|
||
|
|
if (ReferenceEquals(x, y)) return true;
|
||
|
|
if (x is null || y is null) return false;
|
||
|
|
return x.Id == y.Id;
|
||
|
|
}
|
||
|
|
|
||
|
|
public int GetHashCode(TestItem obj) => obj.Id.GetHashCode();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class BatchBySqrtNTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public void BatchBySqrtN_SmallCollection_ReturnsSingleBatch()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(100).ToList();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var batches = items.BatchBySqrtN().ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
batches.Should().HaveCount(10); // sqrt(100) = 10, so 10 batches of 10
|
||
|
|
batches.All(b => b.Count() == 10).Should().BeTrue();
|
||
|
|
batches.SelectMany(b => b).Should().BeEquivalentTo(items);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void BatchBySqrtN_LargeCollection_ReturnsOptimalBatches()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(10_000).ToList();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var batches = items.BatchBySqrtN().ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
var expectedBatchSize = (int)Math.Sqrt(10_000); // 100
|
||
|
|
batches.Should().HaveCount(100);
|
||
|
|
batches.Take(99).All(b => b.Count() == expectedBatchSize).Should().BeTrue();
|
||
|
|
batches.SelectMany(b => b).Should().BeEquivalentTo(items);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void BatchBySqrtN_NonSquareNumber_HandlesRemainder()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(150).ToList();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var batches = items.BatchBySqrtN().ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
var batchSize = (int)Math.Sqrt(150); // 12
|
||
|
|
batches.Should().HaveCount(13); // 12 full batches + 1 partial
|
||
|
|
batches.Take(12).All(b => b.Count() == batchSize).Should().BeTrue();
|
||
|
|
batches.Last().Count().Should().Be(150 - (12 * batchSize));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void BatchBySqrtN_EmptyCollection_ReturnsNoBatches()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = Enumerable.Empty<int>();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var batches = items.BatchBySqrtN().ToList();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
batches.Should().BeEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task BatchBySqrtNAsync_ProcessesAsynchronously()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(1000);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var batchCount = 0;
|
||
|
|
var totalItems = 0;
|
||
|
|
await foreach (var batch in items.BatchBySqrtNAsync())
|
||
|
|
{
|
||
|
|
batchCount++;
|
||
|
|
totalItems += batch.Count();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
batchCount.Should().BeGreaterThan(1);
|
||
|
|
totalItems.Should().Be(1000);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class ToCheckpointedListAsyncTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public async Task ToCheckpointedListAsync_SmallCollection_ReturnsAllItems()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(100);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = await items.ToCheckpointedListAsync();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().HaveCount(100);
|
||
|
|
result.Should().BeEquivalentTo(Enumerable.Range(0, 100));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task ToCheckpointedListAsync_WithCheckpointAction_CallsCheckpoint()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(10_000);
|
||
|
|
var checkpointCount = 0;
|
||
|
|
var lastCheckpointedCount = 0;
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = await items.ToCheckpointedListAsync(
|
||
|
|
checkpointAction: async (list) =>
|
||
|
|
{
|
||
|
|
checkpointCount++;
|
||
|
|
lastCheckpointedCount = list.Count;
|
||
|
|
await Task.Delay(1); // Simulate async work
|
||
|
|
});
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
result.Should().HaveCount(10_000);
|
||
|
|
checkpointCount.Should().BeGreaterThan(0);
|
||
|
|
checkpointCount.Should().BeLessThanOrEqualTo(100); // sqrt(10000) = 100
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task ToCheckpointedListAsync_WithCancellation_ThrowsWhenCancelled()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateNumbers(100_000);
|
||
|
|
var cts = new CancellationTokenSource();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var task = items.ToCheckpointedListAsync(
|
||
|
|
checkpointAction: async (list) =>
|
||
|
|
{
|
||
|
|
if (list.Count > 5000)
|
||
|
|
{
|
||
|
|
cts.Cancel();
|
||
|
|
}
|
||
|
|
await Task.Delay(1);
|
||
|
|
},
|
||
|
|
cancellationToken: cts.Token);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
await task.Invoking(t => t).Should().ThrowAsync<OperationCanceledException>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public class StreamAsJsonAsyncTests
|
||
|
|
{
|
||
|
|
[Fact]
|
||
|
|
public async Task StreamAsJsonAsync_SerializesCorrectly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = GenerateTestItems(10);
|
||
|
|
var stream = new System.IO.MemoryStream();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
await items.StreamAsJsonAsync(stream);
|
||
|
|
stream.Position = 0;
|
||
|
|
var json = new System.IO.StreamReader(stream).ReadToEnd();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
json.Should().StartWith("[");
|
||
|
|
json.Should().EndWith("]");
|
||
|
|
json.Should().Contain("\"Id\"");
|
||
|
|
json.Should().Contain("\"Value\"");
|
||
|
|
json.Should().Contain("\"Category\"");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task StreamAsJsonAsync_EmptyCollection_WritesEmptyArray()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var items = Enumerable.Empty<TestItem>();
|
||
|
|
var stream = new System.IO.MemoryStream();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
await items.StreamAsJsonAsync(stream);
|
||
|
|
stream.Position = 0;
|
||
|
|
var json = new System.IO.StreamReader(stream).ReadToEnd();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
json.Trim().Should().Be("[]");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|