Files

520 lines
16 KiB
C#
Raw Permalink Normal View History

2025-07-20 03:41:39 -04:00
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("[]");
}
}
}