250 lines
7.8 KiB
C#
Executable File
250 lines
7.8 KiB
C#
Executable File
using ZB.MOM.WW.CBDD.Bson;
|
|
using ZB.MOM.WW.CBDD.Core.Compression;
|
|
using ZB.MOM.WW.CBDD.Core.Storage;
|
|
using ZB.MOM.WW.CBDD.Shared;
|
|
using System.Security.Cryptography;
|
|
using System.IO.Compression;
|
|
using System.IO.MemoryMappedFiles;
|
|
|
|
namespace ZB.MOM.WW.CBDD.Tests;
|
|
|
|
public class DbContextTests : IDisposable
|
|
{
|
|
private string _dbPath;
|
|
|
|
/// <summary>
|
|
/// Initializes test file paths for database context tests.
|
|
/// </summary>
|
|
public DbContextTests()
|
|
{
|
|
_dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_{Guid.NewGuid()}.db");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the basic database context lifecycle works.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DbContext_BasicLifecycle_Works()
|
|
{
|
|
using var db = new Shared.TestDbContext(_dbPath);
|
|
|
|
var user = new User { Name = "Alice", Age = 30 };
|
|
var id = db.Users.Insert(user);
|
|
|
|
var found = db.Users.FindById(id);
|
|
found.ShouldNotBeNull();
|
|
found.Name.ShouldBe("Alice");
|
|
found.Age.ShouldBe(30);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies multiple CRUD operations execute correctly in one context.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DbContext_MultipleOperations_Work()
|
|
{
|
|
using var db = new Shared.TestDbContext(_dbPath);
|
|
|
|
// Insert
|
|
var alice = new User { Name = "Alice", Age = 30 };
|
|
var bob = new User { Name = "Bob", Age = 25 };
|
|
|
|
var id1 = db.Users.Insert(alice);
|
|
var id2 = db.Users.Insert(bob);
|
|
|
|
// FindAll
|
|
var all = db.Users.FindAll().ToList();
|
|
all.Count.ShouldBe(2);
|
|
|
|
// Update
|
|
alice.Age = 31;
|
|
db.Users.Update(alice).ShouldBeTrue();
|
|
|
|
var updated = db.Users.FindById(id1);
|
|
updated!.Age.ShouldBe(31);
|
|
|
|
// Delete
|
|
db.Users.Delete(id2).ShouldBeTrue();
|
|
db.Users.Count().ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies disposing and reopening context preserves persisted data.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DbContext_Dispose_ReleasesResources()
|
|
{
|
|
_dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_reopen.db");
|
|
var totalUsers = 0;
|
|
// First context - insert and dispose (auto-checkpoint)
|
|
using (var db = new Shared.TestDbContext(_dbPath))
|
|
{
|
|
db.Users.Insert(new User { Name = "Test", Age = 20 });
|
|
db.SaveChanges(); // Explicitly save changes to ensure data is in WAL
|
|
var beforeCheckpointTotalUsers = db.Users.FindAll().Count();
|
|
db.ForceCheckpoint(); // Force checkpoint to ensure data is persisted to main file
|
|
totalUsers = db.Users.FindAll().Count();
|
|
var countedUsers = db.Users.Count();
|
|
totalUsers.ShouldBe(beforeCheckpointTotalUsers);
|
|
} // Dispose → Commit → ForceCheckpoint → Write to PageFile
|
|
|
|
// Should be able to open again and see persisted data
|
|
using var db2 = new Shared.TestDbContext(_dbPath);
|
|
|
|
totalUsers.ShouldBe(1);
|
|
db2.Users.FindAll().Count().ShouldBe(totalUsers);
|
|
db2.Users.Count().ShouldBe(totalUsers);
|
|
}
|
|
private static string ComputeFileHash(string path)
|
|
{
|
|
using var stream = File.OpenRead(path);
|
|
using var sha256 = SHA256.Create();
|
|
return Convert.ToHexString(sha256.ComputeHash(stream));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies database file size and content change after insert and checkpoint.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DatabaseFile_SizeAndContent_ChangeAfterInsert()
|
|
{
|
|
var dbPath = Path.Combine(Path.GetTempPath(), $"test_dbfile_{Guid.NewGuid()}.db");
|
|
|
|
// 1. Crea e chiudi database vuoto
|
|
using (var db = new Shared.TestDbContext(dbPath))
|
|
{
|
|
db.Users.Insert(new User { Name = "Pippo", Age = 42 });
|
|
}
|
|
var initialSize = new FileInfo(dbPath).Length;
|
|
var initialHash = ComputeFileHash(dbPath);
|
|
|
|
// 2. Riapri, inserisci, chiudi
|
|
using (var db = new Shared.TestDbContext(dbPath))
|
|
{
|
|
db.Users.Insert(new User { Name = "Test", Age = 42 });
|
|
db.ForceCheckpoint(); // Forza persistenza
|
|
}
|
|
var afterInsertSize = new FileInfo(dbPath).Length;
|
|
var afterInsertHash = ComputeFileHash(dbPath);
|
|
|
|
// 3. Verifica che dimensione e hash siano cambiati
|
|
afterInsertSize.ShouldNotBe(initialSize);
|
|
afterInsertHash.ShouldNotBe(initialHash);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the WAL file path is auto-derived from database path.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DbContext_AutoDerivesWalPath()
|
|
{
|
|
using var db = new Shared.TestDbContext(_dbPath);
|
|
db.Users.Insert(new User { Name = "Test", Age = 20 });
|
|
|
|
var walPath = Path.ChangeExtension(_dbPath, ".wal");
|
|
File.Exists(walPath).ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies custom page file and compression options support roundtrip data access.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DbContext_WithCustomPageFileAndCompressionOptions_ShouldSupportRoundTrip()
|
|
{
|
|
var dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_compression_{Guid.NewGuid():N}.db");
|
|
var options = new CompressionOptions
|
|
{
|
|
EnableCompression = true,
|
|
MinSizeBytes = 0,
|
|
MinSavingsPercent = 0,
|
|
Codec = CompressionCodec.Brotli,
|
|
Level = CompressionLevel.Fastest
|
|
};
|
|
|
|
var config = new PageFileConfig
|
|
{
|
|
PageSize = 16 * 1024,
|
|
InitialFileSize = 1024 * 1024,
|
|
Access = MemoryMappedFileAccess.ReadWrite
|
|
};
|
|
|
|
try
|
|
{
|
|
using var db = new Shared.TestDbContext(dbPath, config, options);
|
|
var payload = string.Concat(Enumerable.Repeat("compressible-", 3000));
|
|
var id = db.Users.Insert(new User { Name = payload, Age = 77 });
|
|
db.SaveChanges();
|
|
|
|
var loaded = db.Users.FindById(id);
|
|
loaded.ShouldNotBeNull();
|
|
loaded.Name.ShouldBe(payload);
|
|
db.GetCompressionStats().CompressedDocumentCount.ShouldBeGreaterThanOrEqualTo(1);
|
|
}
|
|
finally
|
|
{
|
|
CleanupDbFiles(dbPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies compact API returns stats and preserves data consistency.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DbContext_CompactApi_ShouldReturnStatsAndPreserveData()
|
|
{
|
|
var dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_compact_{Guid.NewGuid():N}.db");
|
|
try
|
|
{
|
|
using var db = new Shared.TestDbContext(dbPath);
|
|
for (var i = 0; i < 120; i++)
|
|
{
|
|
db.Users.Insert(new User { Name = $"compact-{i:D3}", Age = i % 20 });
|
|
}
|
|
|
|
db.SaveChanges();
|
|
db.Users.Count().ShouldBe(120);
|
|
db.SaveChanges();
|
|
|
|
var stats = db.Compact(new CompactionOptions
|
|
{
|
|
EnableTailTruncation = true,
|
|
DefragmentSlottedPages = true,
|
|
NormalizeFreeList = true
|
|
});
|
|
|
|
stats.OnlineMode.ShouldBeFalse();
|
|
db.Users.Count().ShouldBe(120);
|
|
}
|
|
finally
|
|
{
|
|
CleanupDbFiles(dbPath);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes test resources and cleans up generated files.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
CleanupDbFiles(_dbPath);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
|
|
private static void CleanupDbFiles(string dbPath)
|
|
{
|
|
if (File.Exists(dbPath)) File.Delete(dbPath);
|
|
|
|
var walPath = Path.ChangeExtension(dbPath, ".wal");
|
|
if (File.Exists(walPath)) File.Delete(walPath);
|
|
|
|
var markerPath = $"{dbPath}.compact.state";
|
|
if (File.Exists(markerPath)) File.Delete(markerPath);
|
|
}
|
|
}
|