304 lines
8.9 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
} |