Files
2025-07-20 03:41:39 -04:00

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();
}
}