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(); 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 { "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 { "a", "b", "c" } }; await manager.CreateCheckpointAsync(originalState); // Act var loadedState = await manager.RestoreLatestCheckpointAsync(); // 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(); // 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(); // 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(); // 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(); // 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(); // Assert // Should handle the corrupted file gracefully result.Should().BeNull(); } private class TestState { public int ProcessedCount { get; set; } public List Items { get; set; } = new(); } }