Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SiteLocalDbAutoVacuumTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

176 lines
7.4 KiB
C#

using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.LocalDb;
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
/// <summary>
/// <c>PRAGMA auto_vacuum = INCREMENTAL</c> only takes effect on
/// a table-less database, and <c>SqliteLocalDb</c>'s constructor creates LocalDb's own
/// bookkeeping tables before anything else ever gets a connection to the consolidated site
/// file. <see cref="SiteLocalDbSetup.EnsureAutoVacuumOnFreshFile"/> is the fix: it pre-creates
/// a genuinely fresh file with the pragma set BEFORE <c>AddZbLocalDb</c>'s lazy singleton ever
/// constructs <c>SqliteLocalDb</c>.
/// </summary>
/// <remarks>
/// These tests exercise <see cref="SiteLocalDbSetup.EnsureAutoVacuumOnFreshFile"/> directly
/// (it is <c>internal</c>, visible here via <c>InternalsVisibleTo</c>) rather than through the
/// full <see cref="SiteServiceRegistration"/> composition root — the mechanism under test is a
/// pure ordering/pragma concern that does not depend on the rest of the site graph, and
/// <see cref="SiteLocalDbWiringTests"/> already proves the composition root wires
/// <c>SiteLocalDbSetup</c> in at all.
/// </remarks>
public class SiteLocalDbAutoVacuumTests
{
/// <summary>Verifies that a fresh file reports incremental auto-vacuum.</summary>
[Fact]
public void FreshFile_ReportsIncrementalAutoVacuum()
{
// The plan's own acceptance test: a fresh file reports PRAGMA auto_vacuum = 2
// (INCREMENTAL) after the pre-create step runs.
var path = TempDbPath();
try
{
var config = ConfigFor(path);
SiteLocalDbSetup.EnsureAutoVacuumOnFreshFile(config);
Assert.Equal(2L, ReadAutoVacuum(path));
}
finally
{
Cleanup(path);
}
}
/// <summary>Verifies that incremental auto-vacuum survives LocalDb creating its own bookkeeping tables afterward.</summary>
[Fact]
public void FreshFile_AutoVacuumSurvives_LocalDbCreatingItsOwnBookkeepingTablesAfterward()
{
// The real end-to-end claim: the pre-create step has to run BEFORE AddZbLocalDb ever
// constructs SqliteLocalDb (which creates LocalDb's own tables first, unconditionally,
// in its constructor) for the ordering fix to mean anything. This proves the flag
// baked in by EnsureAutoVacuumOnFreshFile survives that later table creation — the
// exact sequence SiteServiceRegistration.Configure now runs in production.
var path = TempDbPath();
try
{
var config = ConfigFor(path);
SiteLocalDbSetup.EnsureAutoVacuumOnFreshFile(config);
var services = new ServiceCollection();
services.AddZbLocalDb(config, db => SiteLocalDbSetup.OnReady(db, config));
using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
// Resolving ILocalDb is what triggers SqliteLocalDb's constructor (LocalDbSchema
// .EnsureCreated) and then OnReady (SiteEventLogSchema.Apply, etc.) — i.e. every
// real table this file will ever hold. This config carries no
// LocalDb:Replication section, so OnReady takes the deregistration branch and
// db.ReplicatedTables stays empty — check the raw file instead to confirm the
// table genuinely exists.
var db = scope.ServiceProvider.GetRequiredService<ILocalDb>();
Assert.NotNull(db);
Assert.Contains("site_events", TableNames(path));
Assert.Equal(2L, ReadAutoVacuum(path));
}
finally
{
SqliteConnection.ClearAllPools();
Cleanup(path);
}
}
/// <summary>Verifies that an existing file is left alone and its auto-vacuum stays none.</summary>
[Fact]
public void ExistingFile_IsLeftAlone_AutoVacuumStaysNone()
{
// Honesty requirement (plan decision 2): the fix only helps a file that does not exist
// yet. A file that already has tables — every node on the docker rig today, and any
// site database created before this fix shipped — is untouched, and stays
// auto_vacuum = NONE forever short of an offline VACUUM this task deliberately does
// not attempt.
var path = TempDbPath();
try
{
// Pre-populate the file exactly like a real (pre-fix) LocalDb file: at least one
// table exists before EnsureAutoVacuumOnFreshFile ever runs.
using (var seed = new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = path }.ToString()))
{
seed.Open();
using var cmd = seed.CreateCommand();
cmd.CommandText = "CREATE TABLE legacy_marker (id INTEGER PRIMARY KEY)";
cmd.ExecuteNonQuery();
}
Assert.Equal(0L, ReadAutoVacuum(path)); // NONE, the SQLite default.
var config = ConfigFor(path);
SiteLocalDbSetup.EnsureAutoVacuumOnFreshFile(config);
// Still NONE — the method must not attempt VACUUM against a populated file.
Assert.Equal(0L, ReadAutoVacuum(path));
}
finally
{
SqliteConnection.ClearAllPools();
Cleanup(path);
}
}
/// <summary>Verifies that a missing LocalDb path does not throw.</summary>
[Fact]
public void MissingLocalDbPath_DoesNotThrow()
{
// AddZbLocalDb's own options validation is the authoritative place a missing path is
// reported; this pre-create step must defer to it rather than throwing its own,
// differently-worded error first.
var config = new ConfigurationBuilder().AddInMemoryCollection(
new Dictionary<string, string?>()).Build();
var ex = Record.Exception(() => SiteLocalDbSetup.EnsureAutoVacuumOnFreshFile(config));
Assert.Null(ex);
}
private static string TempDbPath() =>
Path.Combine(Path.GetTempPath(), $"scadabridge_autovacuum_{Guid.NewGuid():N}.db");
private static IConfiguration ConfigFor(string path) =>
new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { ["LocalDb:Path"] = path })
.Build();
private static long ReadAutoVacuum(string path)
{
using var connection = new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = path }.ToString());
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "PRAGMA auto_vacuum";
return Convert.ToInt64(cmd.ExecuteScalar());
}
private static HashSet<string> TableNames(string path)
{
using var connection = new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = path }.ToString());
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type = 'table'";
using var reader = cmd.ExecuteReader();
var names = new HashSet<string>(StringComparer.Ordinal);
while (reader.Read()) names.Add(reader.GetString(0));
return names;
}
private static void Cleanup(string path)
{
foreach (var suffix in new[] { "", "-wal", "-shm" })
{
try { File.Delete(path + suffix); } catch { /* best effort */ }
}
}
}