Files
sqrtspace-dotnet/tests/SqrtSpace.SpaceTime.Tests/Core/ExternalStorageTests.cs
2025-07-20 03:41:39 -04:00

304 lines
8.9 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using SqrtSpace.SpaceTime.Core;
using Xunit;
namespace SqrtSpace.SpaceTime.Tests.Core;
public class ExternalStorageTests : IDisposable
{
private readonly string _testDirectory;
private readonly ExternalStorage<TestData> _storage;
public ExternalStorageTests()
{
_testDirectory = Path.Combine(Path.GetTempPath(), "spacetime_external_tests", Guid.NewGuid().ToString());
Directory.CreateDirectory(_testDirectory);
_storage = new ExternalStorage<TestData>(_testDirectory);
}
public void Dispose()
{
_storage?.Dispose();
if (Directory.Exists(_testDirectory))
{
Directory.Delete(_testDirectory, true);
}
}
[Fact]
public async Task SpillToDiskAsync_CreatesSpillFile()
{
// Arrange
var data = new List<TestData>
{
new TestData { Id = 1, Name = "First", Value = 10.5 },
new TestData { Id = 2, Name = "Second", Value = 20.5 },
new TestData { Id = 3, Name = "Third", Value = 30.5 }
};
// Act
var spillFile = await _storage.SpillToDiskAsync(data);
// Assert
spillFile.Should().NotBeNullOrEmpty();
File.Exists(spillFile).Should().BeTrue();
var files = Directory.GetFiles(_testDirectory);
files.Should().HaveCount(1);
}
[Fact]
public async Task ReadFromDiskAsync_ReturnsSpilledData()
{
// Arrange
var originalData = new List<TestData>
{
new TestData { Id = 1, Name = "First", Value = 10.5 },
new TestData { Id = 2, Name = "Second", Value = 20.5 },
new TestData { Id = 3, Name = "Third", Value = 30.5 }
};
var spillFile = await _storage.SpillToDiskAsync(originalData);
// Act
var readData = new List<TestData>();
await foreach (var item in _storage.ReadFromDiskAsync(spillFile))
{
readData.Add(item);
}
// Assert
readData.Should().HaveCount(3);
readData.Should().BeEquivalentTo(originalData);
}
[Fact]
public async Task MergeSpillFilesAsync_MergesMultipleFiles()
{
// Arrange
var data1 = new List<TestData>
{
new TestData { Id = 1, Name = "A" },
new TestData { Id = 3, Name = "C" },
new TestData { Id = 5, Name = "E" }
};
var data2 = new List<TestData>
{
new TestData { Id = 2, Name = "B" },
new TestData { Id = 4, Name = "D" },
new TestData { Id = 6, Name = "F" }
};
await _storage.SpillToDiskAsync(data1);
await _storage.SpillToDiskAsync(data2);
// Act
var merged = new List<TestData>();
var comparer = Comparer<TestData>.Create((a, b) => a.Id.CompareTo(b.Id));
await foreach (var item in _storage.MergeSpillFilesAsync(comparer))
{
merged.Add(item);
}
// Assert
merged.Should().HaveCount(6);
merged.Select(x => x.Id).Should().BeInAscendingOrder();
merged.Select(x => x.Name).Should().Equal("A", "B", "C", "D", "E", "F");
}
[Fact]
public void GetSpillSize_ReturnsCorrectSize()
{
// Act
var size = _storage.GetSpillSize();
// Assert
size.Should().BeLessThanOrEqualTo(0);
}
[Fact]
public async Task GetSpillSize_AfterSpilling_ReturnsNonZeroSize()
{
// Arrange
var data = Enumerable.Range(1, 100).Select(i => new TestData
{
Id = i,
Name = $"Item {i}",
Value = i * 1.5
}).ToList();
// Act
await _storage.SpillToDiskAsync(data);
var size = _storage.GetSpillSize();
// Assert
size.Should().BeGreaterThan(0);
}
[Fact]
public async Task SpillToDiskAsync_LargeDataSet_HandlesCorrectly()
{
// Arrange
var largeData = Enumerable.Range(1, 10000).Select(i => new TestData
{
Id = i,
Name = $"Item {i}",
Value = i * 1.5,
Description = new string('x', 100) // Add some bulk
}).ToList();
// Act
var spillFile = await _storage.SpillToDiskAsync(largeData);
// Assert
File.Exists(spillFile).Should().BeTrue();
var fileInfo = new FileInfo(spillFile);
fileInfo.Length.Should().BeGreaterThan(1000); // Should be reasonably large
}
[Fact]
public async Task ReadFromDiskAsync_NonExistentFile_ThrowsException()
{
// Arrange
var nonExistentFile = Path.Combine(_testDirectory, "does_not_exist.bin");
// Act & Assert
await Assert.ThrowsAsync<FileNotFoundException>(async () =>
{
await foreach (var item in _storage.ReadFromDiskAsync(nonExistentFile))
{
// Should throw before getting here
}
});
}
[Fact]
public async Task MergeSpillFilesAsync_EmptyStorage_ReturnsEmpty()
{
// Act
var merged = new List<TestData>();
var comparer = Comparer<TestData>.Create((a, b) => a.Id.CompareTo(b.Id));
await foreach (var item in _storage.MergeSpillFilesAsync(comparer))
{
merged.Add(item);
}
// Assert
merged.Should().BeEmpty();
}
[Fact]
public async Task SpillToDiskAsync_MultipleSpills_CreatesMultipleFiles()
{
// Arrange
var data1 = new List<TestData> { new TestData { Id = 1 } };
var data2 = new List<TestData> { new TestData { Id = 2 } };
var data3 = new List<TestData> { new TestData { Id = 3 } };
// Act
await _storage.SpillToDiskAsync(data1);
await _storage.SpillToDiskAsync(data2);
await _storage.SpillToDiskAsync(data3);
// Assert
var files = Directory.GetFiles(_testDirectory);
files.Should().HaveCount(3);
}
[Fact]
public void Dispose_RemovesSpillFiles()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), "spacetime_dispose_test", Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir);
using (var storage = new ExternalStorage<TestData>(tempDir))
{
// Create some spill files
storage.SpillToDiskAsync(new List<TestData> { new TestData { Id = 1 } }).Wait();
Directory.GetFiles(tempDir).Should().NotBeEmpty();
}
// Act & Assert - files should be cleaned up after disposal
Directory.GetFiles(tempDir).Should().BeEmpty();
Directory.Delete(tempDir);
}
[Fact]
public async Task SpillToDiskAsync_WithCustomSerializer_SerializesCorrectly()
{
// Arrange
var customSerializer = new CustomSerializer();
var storage = new ExternalStorage<TestData>(_testDirectory, customSerializer);
var data = new List<TestData>
{
new TestData { Id = 1, Name = "Custom" }
};
// Act
var spillFile = await storage.SpillToDiskAsync(data);
// Assert
File.Exists(spillFile).Should().BeTrue();
// The custom serializer should have been used
customSerializer.SerializeCalled.Should().BeTrue();
}
[Fact]
public async Task ConcurrentSpills_HandledSafely()
{
// Arrange
var tasks = new List<Task<string>>();
// Act
for (int i = 0; i < 10; i++)
{
var index = i;
var task = Task.Run(async () =>
{
var data = new List<TestData> { new TestData { Id = index } };
return await _storage.SpillToDiskAsync(data);
});
tasks.Add(task);
}
var spillFiles = await Task.WhenAll(tasks);
// Assert
spillFiles.Should().HaveCount(10);
spillFiles.Should().OnlyHaveUniqueItems();
spillFiles.All(f => File.Exists(f)).Should().BeTrue();
}
private class TestData
{
public int Id { get; set; }
public string Name { get; set; } = "";
public double Value { get; set; }
public string? Description { get; set; }
}
private class CustomSerializer : ISerializer<TestData>
{
public bool SerializeCalled { get; private set; }
public async Task SerializeAsync(Stream stream, IEnumerable<TestData> data)
{
SerializeCalled = true;
var defaultSerializer = new JsonSerializer<TestData>();
await defaultSerializer.SerializeAsync(stream, data);
}
public async IAsyncEnumerable<TestData> DeserializeAsync(Stream stream)
{
var defaultSerializer = new JsonSerializer<TestData>();
await foreach (var item in defaultSerializer.DeserializeAsync(stream))
{
yield return item;
}
}
}
}