390 lines
12 KiB
C#
390 lines
12 KiB
C#
|
|
using System;
|
||
|
|
using System.Collections.Generic;
|
||
|
|
using System.IO;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Threading;
|
||
|
|
using System.Threading.Tasks;
|
||
|
|
using FluentAssertions;
|
||
|
|
using Moq;
|
||
|
|
using SqrtSpace.SpaceTime.Core;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace SqrtSpace.SpaceTime.Tests.Core;
|
||
|
|
|
||
|
|
public class CheckpointManagerTests : IDisposable
|
||
|
|
{
|
||
|
|
private readonly string _testDirectory;
|
||
|
|
|
||
|
|
public CheckpointManagerTests()
|
||
|
|
{
|
||
|
|
_testDirectory = Path.Combine(Path.GetTempPath(), "spacetime_tests", Guid.NewGuid().ToString());
|
||
|
|
Directory.CreateDirectory(_testDirectory);
|
||
|
|
}
|
||
|
|
|
||
|
|
public void Dispose()
|
||
|
|
{
|
||
|
|
if (Directory.Exists(_testDirectory))
|
||
|
|
{
|
||
|
|
Directory.Delete(_testDirectory, true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Constructor_CreatesCheckpointDirectory()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var checkpointPath = Path.Combine(_testDirectory, "checkpoints");
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var manager = new CheckpointManager(checkpointPath);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
Directory.Exists(checkpointPath).Should().BeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void ShouldCheckpoint_WithSqrtNStrategy_ChecksCorrectly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(
|
||
|
|
_testDirectory,
|
||
|
|
strategy: CheckpointStrategy.SqrtN,
|
||
|
|
totalOperations: 100);
|
||
|
|
|
||
|
|
// Act & Assert
|
||
|
|
// For 100 items, sqrt(100) = 10, so checkpoint every 10 items
|
||
|
|
bool shouldCheckpoint10 = false, shouldCheckpoint20 = false;
|
||
|
|
for (int i = 1; i <= 20; i++)
|
||
|
|
{
|
||
|
|
var shouldCheckpoint = manager.ShouldCheckpoint();
|
||
|
|
if (i == 10) shouldCheckpoint10 = shouldCheckpoint;
|
||
|
|
if (i == 20) shouldCheckpoint20 = shouldCheckpoint;
|
||
|
|
}
|
||
|
|
|
||
|
|
shouldCheckpoint10.Should().BeTrue();
|
||
|
|
shouldCheckpoint20.Should().BeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void ShouldCheckpoint_WithLinearStrategy_ChecksCorrectly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(
|
||
|
|
_testDirectory,
|
||
|
|
strategy: CheckpointStrategy.Linear);
|
||
|
|
|
||
|
|
// Act & Assert
|
||
|
|
// Linear strategy checkpoints every 1000 operations
|
||
|
|
bool checkpoint999 = false, checkpoint1000 = false;
|
||
|
|
for (int i = 1; i <= 1000; i++)
|
||
|
|
{
|
||
|
|
var shouldCheckpoint = manager.ShouldCheckpoint();
|
||
|
|
if (i == 999) checkpoint999 = shouldCheckpoint;
|
||
|
|
if (i == 1000) checkpoint1000 = shouldCheckpoint;
|
||
|
|
}
|
||
|
|
|
||
|
|
checkpoint999.Should().BeFalse();
|
||
|
|
checkpoint1000.Should().BeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void ShouldCheckpoint_WithLogarithmicStrategy_ChecksCorrectly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(
|
||
|
|
_testDirectory,
|
||
|
|
strategy: CheckpointStrategy.Logarithmic);
|
||
|
|
|
||
|
|
// Act & Assert
|
||
|
|
// Logarithmic checkpoints at powers of 2
|
||
|
|
var results = new List<bool>();
|
||
|
|
for (int i = 1; i <= 8; i++)
|
||
|
|
{
|
||
|
|
results.Add(manager.ShouldCheckpoint());
|
||
|
|
}
|
||
|
|
|
||
|
|
results[0].Should().BeTrue(); // 1 is power of 2
|
||
|
|
results[1].Should().BeTrue(); // 2 is power of 2
|
||
|
|
results[2].Should().BeFalse(); // 3 is not
|
||
|
|
results[3].Should().BeTrue(); // 4 is power of 2
|
||
|
|
results[4].Should().BeFalse(); // 5 is not
|
||
|
|
results[5].Should().BeFalse(); // 6 is not
|
||
|
|
results[6].Should().BeFalse(); // 7 is not
|
||
|
|
results[7].Should().BeTrue(); // 8 is power of 2
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void ShouldCheckpoint_WithNoneStrategy_AlwaysFalse()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(
|
||
|
|
_testDirectory,
|
||
|
|
strategy: CheckpointStrategy.None);
|
||
|
|
|
||
|
|
// Act & Assert
|
||
|
|
for (int i = 1; i <= 100; i++)
|
||
|
|
{
|
||
|
|
manager.ShouldCheckpoint().Should().BeFalse();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task CreateCheckpointAsync_CreatesCheckpointFile()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory);
|
||
|
|
var state = new TestState
|
||
|
|
{
|
||
|
|
ProcessedCount = 42,
|
||
|
|
Items = new List<string> { "item1", "item2", "item3" }
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
await manager.CreateCheckpointAsync(state);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
var checkpointFiles = Directory.GetFiles(_testDirectory, "checkpoint_*.json");
|
||
|
|
checkpointFiles.Should().HaveCount(1);
|
||
|
|
|
||
|
|
var content = await File.ReadAllTextAsync(checkpointFiles[0]);
|
||
|
|
content.Should().Contain("processedCount");
|
||
|
|
content.Should().Contain("42");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task CreateCheckpointAsync_WithCheckpointId_UsesSpecificId()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory);
|
||
|
|
var state = new TestState { ProcessedCount = 10 };
|
||
|
|
var checkpointId = "custom_checkpoint_123";
|
||
|
|
|
||
|
|
// Act
|
||
|
|
await manager.CreateCheckpointAsync(state, checkpointId);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
var checkpointFile = Path.Combine(_testDirectory, $"{checkpointId}.json");
|
||
|
|
File.Exists(checkpointFile).Should().BeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task RestoreLatestCheckpointAsync_RestoresState()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory);
|
||
|
|
var originalState = new TestState
|
||
|
|
{
|
||
|
|
ProcessedCount = 100,
|
||
|
|
Items = new List<string> { "a", "b", "c" }
|
||
|
|
};
|
||
|
|
await manager.CreateCheckpointAsync(originalState);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var loadedState = await manager.RestoreLatestCheckpointAsync<TestState>();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
loadedState.Should().NotBeNull();
|
||
|
|
loadedState!.ProcessedCount.Should().Be(100);
|
||
|
|
loadedState.Items.Should().BeEquivalentTo(new[] { "a", "b", "c" });
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task RestoreLatestCheckpointAsync_WithNoCheckpoint_ReturnsNull()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var loadedState = await manager.RestoreLatestCheckpointAsync<TestState>();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
loadedState.Should().BeNull();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task RestoreLatestCheckpointAsync_RestoresLatestOnly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory);
|
||
|
|
var state1 = new TestState { ProcessedCount = 10 };
|
||
|
|
var state2 = new TestState { ProcessedCount = 20 };
|
||
|
|
|
||
|
|
await manager.CreateCheckpointAsync(state1, "checkpoint1");
|
||
|
|
await Task.Delay(100); // Ensure different timestamps
|
||
|
|
await manager.CreateCheckpointAsync(state2, "checkpoint2");
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var loaded = await manager.RestoreLatestCheckpointAsync<TestState>();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
loaded!.ProcessedCount.Should().Be(20);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task RestoreLatestCheckpointAsync_AfterMultipleCheckpoints_RestoresNewest()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory);
|
||
|
|
|
||
|
|
await manager.CreateCheckpointAsync(new TestState { ProcessedCount = 10 });
|
||
|
|
await Task.Delay(100); // Ensure different timestamps
|
||
|
|
await manager.CreateCheckpointAsync(new TestState { ProcessedCount = 20 });
|
||
|
|
await Task.Delay(100);
|
||
|
|
await manager.CreateCheckpointAsync(new TestState { ProcessedCount = 30 });
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var latest = await manager.RestoreLatestCheckpointAsync<TestState>();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
latest.Should().NotBeNull();
|
||
|
|
latest!.ProcessedCount.Should().Be(30);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task CreateCheckpointAsync_WithMultipleCheckpoints_CreatesMultipleFiles()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory);
|
||
|
|
|
||
|
|
await manager.CreateCheckpointAsync(new TestState { ProcessedCount = 10 }, "cp1");
|
||
|
|
await manager.CreateCheckpointAsync(new TestState { ProcessedCount = 20 }, "cp2");
|
||
|
|
await manager.CreateCheckpointAsync(new TestState { ProcessedCount = 30 }, "cp3");
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var checkpointFiles = Directory.GetFiles(_testDirectory, "*.json");
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
checkpointFiles.Should().HaveCount(3);
|
||
|
|
checkpointFiles.Should().Contain(f => f.Contains("cp1"));
|
||
|
|
checkpointFiles.Should().Contain(f => f.Contains("cp2"));
|
||
|
|
checkpointFiles.Should().Contain(f => f.Contains("cp3"));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Dispose_RemovesCheckpointDirectory()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
string? tempDir = null;
|
||
|
|
using (var manager = new CheckpointManager())
|
||
|
|
{
|
||
|
|
// Get the checkpoint directory through reflection
|
||
|
|
var dirField = manager.GetType().GetField("_checkpointDirectory", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||
|
|
tempDir = dirField?.GetValue(manager) as string;
|
||
|
|
|
||
|
|
// Verify directory was created
|
||
|
|
Directory.Exists(tempDir).Should().BeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Act & Assert - directory should be deleted after disposal
|
||
|
|
Directory.Exists(tempDir).Should().BeFalse();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task CreateCheckpointAsync_WithSqrtNStrategy_AutoCleansOldCheckpoints()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory, CheckpointStrategy.SqrtN, totalOperations: 100);
|
||
|
|
|
||
|
|
// Create checkpoints - with sqrt(100) = 10, it should keep only ~10 checkpoints
|
||
|
|
for (int i = 1; i <= 20; i++)
|
||
|
|
{
|
||
|
|
// Simulate operations to trigger checkpointing
|
||
|
|
for (int j = 0; j < 10; j++)
|
||
|
|
{
|
||
|
|
if (manager.ShouldCheckpoint())
|
||
|
|
{
|
||
|
|
await manager.CreateCheckpointAsync(new TestState { ProcessedCount = i * 10 + j });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Assert - should have cleaned up old checkpoints automatically
|
||
|
|
var checkpointFiles = Directory.GetFiles(_testDirectory, "*.json");
|
||
|
|
checkpointFiles.Length.Should().BeLessThanOrEqualTo(15); // Allow some buffer
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void OperationsSinceLastCheckpoint_TracksCorrectly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory, CheckpointStrategy.SqrtN, totalOperations: 100);
|
||
|
|
|
||
|
|
// Act & Assert
|
||
|
|
// With sqrt(100) = 10, checkpoints every 10 operations
|
||
|
|
for (int i = 1; i <= 15; i++)
|
||
|
|
{
|
||
|
|
manager.ShouldCheckpoint();
|
||
|
|
|
||
|
|
if (i <= 10)
|
||
|
|
{
|
||
|
|
manager.OperationsSinceLastCheckpoint.Should().Be(i % 10);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
manager.OperationsSinceLastCheckpoint.Should().Be(i - 10);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task CreateCheckpointAsync_ConcurrentWrites_HandledSafely()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory);
|
||
|
|
var tasks = new List<Task>();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
for (int i = 0; i < 10; i++)
|
||
|
|
{
|
||
|
|
var index = i;
|
||
|
|
tasks.Add(Task.Run(async () =>
|
||
|
|
{
|
||
|
|
await manager.CreateCheckpointAsync(new TestState { ProcessedCount = index });
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
await Task.WhenAll(tasks);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
var checkpointFiles = Directory.GetFiles(_testDirectory, "*.json");
|
||
|
|
checkpointFiles.Should().HaveCount(10);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task CreateCheckpointAsync_ReturnsCheckpointPath()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory);
|
||
|
|
var state = new TestState { ProcessedCount = 42 };
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var checkpointPath = await manager.CreateCheckpointAsync(state);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
checkpointPath.Should().NotBeNullOrEmpty();
|
||
|
|
File.Exists(checkpointPath).Should().BeTrue();
|
||
|
|
checkpointPath.Should().EndWith(".json");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task RestoreLatestCheckpointAsync_WithCorruptedFile_ReturnsNull()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var manager = new CheckpointManager(_testDirectory);
|
||
|
|
var corruptedFile = Path.Combine(_testDirectory, "checkpoint_corrupt.json");
|
||
|
|
await File.WriteAllTextAsync(corruptedFile, "{ invalid json");
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var result = await manager.RestoreLatestCheckpointAsync<TestState>();
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
// Should handle the corrupted file gracefully
|
||
|
|
result.Should().BeNull();
|
||
|
|
}
|
||
|
|
|
||
|
|
private class TestState
|
||
|
|
{
|
||
|
|
public int ProcessedCount { get; set; }
|
||
|
|
public List<string> Items { get; set; } = new();
|
||
|
|
}
|
||
|
|
}
|