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.
This commit is contained in:
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for SiteEventLogging-010: previously untested behaviours —
|
||||
/// Regression tests for Previously untested behaviours —
|
||||
/// the query service error path and the recorder's disposed-state semantics.
|
||||
/// </summary>
|
||||
public class EventLogCoverageTests : IDisposable
|
||||
@@ -13,12 +13,14 @@ public class EventLogCoverageTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly TestLocalDb _localDb;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>EventLogCoverageTests</c> class.</summary>
|
||||
public EventLogCoverageTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_coverage_{Guid.NewGuid()}.db");
|
||||
_localDb = TestLocalDb.Create(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_localDb.Dispose();
|
||||
@@ -46,6 +48,7 @@ public class EventLogCoverageTests : IDisposable
|
||||
PageSize: 500,
|
||||
Timestamp: DateTimeOffset.UtcNow);
|
||||
|
||||
/// <summary>Verifies that execute query returns failure response when database unavailable.</summary>
|
||||
[Fact]
|
||||
public void ExecuteQuery_ReturnsFailureResponse_WhenDatabaseUnavailable()
|
||||
{
|
||||
@@ -70,10 +73,12 @@ public class EventLogCoverageTests : IDisposable
|
||||
Assert.Equal("corr-err", response.CorrelationId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that log event async after dispose faults task not reports success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_AfterDispose_FaultsTask_NotReportsSuccess()
|
||||
{
|
||||
// SiteEventLogging-012: when the logger has been disposed the event cannot
|
||||
// When the logger has been disposed the event cannot
|
||||
// be persisted. The returned Task must FAULT (not complete successfully) so
|
||||
// an awaiting caller can distinguish a dropped audit event from a written
|
||||
// one. Per the XML doc contract, the Task "faults if the write fails".
|
||||
@@ -86,6 +91,7 @@ public class EventLogCoverageTests : IDisposable
|
||||
() => task.WaitAsync(TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that dispose is idempotent.</summary>
|
||||
[Fact]
|
||||
public void Dispose_IsIdempotent()
|
||||
{
|
||||
@@ -95,10 +101,12 @@ public class EventLogCoverageTests : IDisposable
|
||||
logger.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that log event async enqueued then disposed faults task when write cannot complete.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_EnqueuedThenDisposed_FaultsTask_WhenWriteCannotComplete()
|
||||
{
|
||||
// SiteEventLogging-012, second path: an event enqueued onto the background
|
||||
// Second path: an event enqueued onto the background
|
||||
// writer just before disposal must NOT be reported as persisted if the
|
||||
// writer's WithConnection returns false (logger disposed mid-drain). We
|
||||
// flood the queue and dispose immediately; any event whose write did not
|
||||
|
||||
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for SiteEventLogging-010: the actor message contract of
|
||||
/// Regression tests for the actor message contract of
|
||||
/// <see cref="EventLogHandlerActor"/> was previously untested.
|
||||
/// </summary>
|
||||
public class EventLogHandlerActorTests : TestKit
|
||||
@@ -14,11 +14,15 @@ public class EventLogHandlerActorTests : TestKit
|
||||
private sealed class FakeQueryService : IEventLogQueryService
|
||||
{
|
||||
private readonly Func<EventLogQueryRequest, EventLogQueryResponse> _handler;
|
||||
/// <summary>Gets the most recent query request received.</summary>
|
||||
public EventLogQueryRequest? LastRequest { get; private set; }
|
||||
|
||||
/// <summary>Initializes the fake with a handler that produces query responses.</summary>
|
||||
/// <param name="handler">The function that answers each query request.</param>
|
||||
public FakeQueryService(Func<EventLogQueryRequest, EventLogQueryResponse> handler)
|
||||
=> _handler = handler;
|
||||
|
||||
/// <inheritdoc />
|
||||
public EventLogQueryResponse ExecuteQuery(EventLogQueryRequest request)
|
||||
{
|
||||
LastRequest = request;
|
||||
@@ -49,6 +53,7 @@ public class EventLogHandlerActorTests : TestKit
|
||||
ErrorMessage: success ? null : "boom",
|
||||
Timestamp: DateTimeOffset.UtcNow);
|
||||
|
||||
/// <summary>Verifies that the actor replies to the sender with the query response.</summary>
|
||||
[Fact]
|
||||
public void Actor_RepliesToSender_WithQueryResponse()
|
||||
{
|
||||
@@ -65,6 +70,7 @@ public class EventLogHandlerActorTests : TestKit
|
||||
Assert.Same(request, fake.LastRequest);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the actor passes a query-service error response on to the sender.</summary>
|
||||
[Fact]
|
||||
public void Actor_PropagatesQueryServiceErrorResponse_ToSender()
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
private readonly SiteEventLogOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// SiteEventLogging-023: stop flag for the concurrent stress test. Declared as
|
||||
/// Stop flag for the concurrent stress test. Declared as
|
||||
/// a <c>volatile</c> field so every writer thread observes the main thread's
|
||||
/// `_stop = true` write without depending on JIT/runtime quirks. A plain
|
||||
/// <c>bool</c> local would be legal-cached in a register inside the tight
|
||||
@@ -21,6 +21,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
/// </summary>
|
||||
private volatile bool _stop;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="EventLogPurgeServiceTests"/> class with a temporary LocalDb file.</summary>
|
||||
public EventLogPurgeServiceTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_purge_{Guid.NewGuid()}.db");
|
||||
@@ -37,6 +38,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
_localDb.Db);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_eventLogger.Dispose();
|
||||
@@ -85,6 +87,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the retention purge deletes events older than the retention window.</summary>
|
||||
[Fact]
|
||||
public void PurgeByRetention_DeletesOldEvents()
|
||||
{
|
||||
@@ -98,6 +101,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(1, GetEventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the retention purge keeps recent events.</summary>
|
||||
[Fact]
|
||||
public void PurgeByRetention_KeepsRecentEvents()
|
||||
{
|
||||
@@ -111,10 +115,11 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(3, GetEventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the retention purge deletes every expired row across multiple batch slices when the backlog exceeds one batch.</summary>
|
||||
[Fact]
|
||||
public void PurgeByRetention_OverBatchSize_DeletesAllExpiredRowsAcrossSlices()
|
||||
{
|
||||
// WP1.7: the retention purge must slice a large expired backlog into
|
||||
// The retention purge must slice a large expired backlog into
|
||||
// bounded DELETE batches (1000 rows/iteration, mirroring the storage-cap
|
||||
// purge a few lines below in the same file) rather than one unbounded
|
||||
// DELETE. Seed more than one slice's worth of expired rows plus a
|
||||
@@ -145,6 +150,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(2, GetEventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the storage-cap purge deletes the oldest events when the database is over the cap.</summary>
|
||||
[Fact]
|
||||
public void PurgeByStorageCap_DeletesOldestWhenOverCap()
|
||||
{
|
||||
@@ -169,6 +175,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(0, GetEventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the database size query returns a positive value.</summary>
|
||||
[Fact]
|
||||
public void GetDatabaseSizeBytes_ReturnsPositiveValue()
|
||||
{
|
||||
@@ -241,11 +248,11 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the storage-cap purge stops once under the cap and does not empty the table.</summary>
|
||||
[Fact]
|
||||
public void PurgeByStorageCap_StopsWhenUnderCap_DoesNotEmptyTable()
|
||||
{
|
||||
// Regression test for SiteEventLogging-001 / -002:
|
||||
// a realistic non-zero cap must trim the oldest events to the budget,
|
||||
// Regression test: a realistic non-zero cap must trim the oldest events to the budget,
|
||||
// not delete the entire table.
|
||||
InsertBulkEvents(3000);
|
||||
|
||||
@@ -275,10 +282,11 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
$"Database size {finalSize} must be at or below cap {finalCapBytes} after purge.");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the storage-cap purge removes the oldest events first.</summary>
|
||||
[Fact]
|
||||
public void PurgeByStorageCap_RemovesOldestEventsFirst()
|
||||
{
|
||||
// Regression test for SiteEventLogging-002: only the oldest events should be
|
||||
// Regression test: only the oldest events should be
|
||||
// removed when trimming to the cap.
|
||||
//
|
||||
// "Oldest" used to mean "lowest autoincrement id". Since LocalDb Phase 1 the id
|
||||
@@ -320,10 +328,12 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(1L, newestPresent);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that starting the service does not block on the initial purge.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task StartAsync_DoesNotBlock_OnTheInitialPurge()
|
||||
{
|
||||
// SiteEventLogging-014 (re-triaged): on .NET 8+ BackgroundService runs
|
||||
// On .NET 8+ BackgroundService runs
|
||||
// ExecuteAsync on a thread-pool thread — the synchronous prelude (the
|
||||
// initial RunPurge()) does NOT execute on the host startup thread, so
|
||||
// StartAsync returns promptly and host startup / the /health/ready gate is
|
||||
@@ -356,10 +366,12 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
await purge.StopAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that ExecuteAsync yields before running the initial purge synchronously.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_YieldsBeforeRunningTheInitialPurgeSynchronously()
|
||||
{
|
||||
// arch-review 2026-08-19 P4-5 — timing-independent companion to
|
||||
// Timing-independent companion to
|
||||
// StartAsync_DoesNotBlock_OnTheInitialPurge above.
|
||||
//
|
||||
// That test times BackgroundService.StartAsync as a whole and passes even on the
|
||||
@@ -368,7 +380,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
// never blocks regardless of whether ExecuteAsync yields first. Verified empirically
|
||||
// against this repo's actual net10.0 target (a BackgroundService whose ExecuteAsync
|
||||
// does Thread.Sleep(1500) before its first await still returns from StartAsync in
|
||||
// ~2 ms). So P4-5's literal "StartAsync blocks" symptom does not reproduce here, and
|
||||
// ~2 ms). So the literal "StartAsync blocks" symptom does not reproduce here, and
|
||||
// a wall-clock test cannot prove the fix.
|
||||
//
|
||||
// What CAN be proven without any timing is the ExecuteAsync-level contract itself:
|
||||
@@ -432,12 +444,13 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a throwing active-node check is logged as inactive rather than active.</summary>
|
||||
[Fact]
|
||||
public void RunPurge_ActiveNodeCheckThrows_TickLogsAsInactive_NotActive()
|
||||
{
|
||||
// arch-review 2026-08-19 P4-5: a throwing isActive predicate must fail CLOSED
|
||||
// A throwing isActive predicate must fail CLOSED
|
||||
// (logged as inactive) rather than silently claiming "active". The predicate no
|
||||
// longer gates any purge work (P4-4 — every node purges its own file every tick
|
||||
// longer gates any purge work (every node purges its own file every tick
|
||||
// regardless), so a wrong value here cannot skip work; but an operator correlating
|
||||
// this line against the real active-node state must not be misled into thinking the
|
||||
// check actually succeeded and reported active.
|
||||
@@ -459,10 +472,12 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Contains("active=False", tickLine.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent writes during a storage-cap purge do not corrupt the connection.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task PurgeByStorageCap_ConcurrentWritesDoNotCorruptConnection()
|
||||
{
|
||||
// Regression test for SiteEventLogging-003: purge running on a background
|
||||
// Regression test: purge running on a background
|
||||
// thread while events are recorded on other threads must not throw
|
||||
// "DataReader already open" / "connection busy" from a shared connection.
|
||||
InsertBulkEvents(2000);
|
||||
@@ -477,7 +492,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
};
|
||||
|
||||
var exceptions = new System.Collections.Concurrent.ConcurrentBag<Exception>();
|
||||
// SiteEventLogging-023: must be volatile so the writer threads observe the
|
||||
// Must be volatile so the writer threads observe the
|
||||
// main thread's `stop = true` flip after the purge task completes. Without
|
||||
// it, a release-mode JIT is permitted to cache the `stop = false` read in
|
||||
// a register inside the tight `while (!stop)` loop and never see the flip,
|
||||
@@ -514,15 +529,16 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Empty(exceptions);
|
||||
}
|
||||
|
||||
// ── arch-review 2026-08-19 P4-4: purge runs on every node, not just the active one ──
|
||||
// (supersedes the old SiteEventLogging-019 "active-node-only gate" — DataConnectionActor
|
||||
// ── Purge runs on every node, not just the active one ──
|
||||
// (supersedes the old "active-node-only gate" — DataConnectionActor
|
||||
// writes site_events on both site nodes, so a standby that skipped its own purge would
|
||||
// grow its file unbounded on an unreplicated site.)
|
||||
|
||||
/// <summary>Verifies that a standby node still runs the purge.</summary>
|
||||
[Fact]
|
||||
public void RunPurge_OnStandbyNode_StillRunsThePurge()
|
||||
{
|
||||
// P4-4 regression: before the fix, a standby (isActiveNode returning false) early-exited
|
||||
// Regression: before the fix, a standby (isActiveNode returning false) early-exited
|
||||
// the purge tick entirely, leaving this well-past-retention row in place forever on an
|
||||
// unreplicated standby. The gate is gone — the standby purges its own file exactly like
|
||||
// the active node.
|
||||
@@ -535,6 +551,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(0, GetEventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an active node runs the retention purge.</summary>
|
||||
[Fact]
|
||||
public void RunPurge_OnActiveNode_RunsTheRetentionPurge()
|
||||
{
|
||||
@@ -549,11 +566,12 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(1, GetEventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the purge still runs when no active-node check is supplied.</summary>
|
||||
[Fact]
|
||||
public void RunPurge_WithNullCheck_StillRuns()
|
||||
{
|
||||
// When no active-node check is supplied (the default for non-clustered hosts and
|
||||
// pre-existing tests), the service runs the purge — unaffected by P4-4 since the
|
||||
// pre-existing tests), the service runs the purge — unaffected by the every-node purge since the
|
||||
// check was never gating anything for this caller either.
|
||||
InsertEventWithTimestamp(DateTimeOffset.UtcNow.AddDays(-31));
|
||||
|
||||
@@ -573,6 +591,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
/// </summary>
|
||||
private const string BacklogNoteFragment = "transient LocalDb oplog backlog";
|
||||
|
||||
/// <summary>Verifies that the retention purge on a replicated node logs the oplog backlog note.</summary>
|
||||
[Fact]
|
||||
public void PurgeByRetention_OnReplicatedNode_LogsTheOplogBacklogNote()
|
||||
{
|
||||
@@ -592,6 +611,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(LogLevel.Information, note.Level);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the retention purge on an unreplicated node does not log the backlog note.</summary>
|
||||
[Fact]
|
||||
public void PurgeByRetention_OnUnreplicatedNode_DoesNotLogTheNote()
|
||||
{
|
||||
@@ -608,6 +628,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.DoesNotContain(logger.Entries, e => e.Message.Contains(BacklogNoteFragment, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the retention purge does not log the backlog note when no rows were deleted.</summary>
|
||||
[Fact]
|
||||
public void PurgeByRetention_WithNoRowsDeleted_DoesNotLogTheNote()
|
||||
{
|
||||
@@ -624,6 +645,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.DoesNotContain(logger.Entries, e => e.Message.Contains(BacklogNoteFragment, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the storage-cap purge on a replicated node logs the oplog backlog note.</summary>
|
||||
[Fact]
|
||||
public void PurgeByStorageCap_OnReplicatedNode_LogsTheOplogBacklogNote()
|
||||
{
|
||||
@@ -650,6 +672,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(LogLevel.Information, note.Level);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the backlog note is logged once per tick when both purge phases delete rows.</summary>
|
||||
[Fact]
|
||||
public void RunPurge_WhenBothPhasesDeleteRows_LogsTheBacklogNoteOnceForTheWholeTick()
|
||||
{
|
||||
@@ -691,6 +714,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Contains($"Purged {expiredRows + recentRows} site_events rows", note.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the purge still runs and swallows the error when the replication check throws.</summary>
|
||||
[Fact]
|
||||
public void RunPurge_WhenReplicationCheckThrows_StillPurgesAndSwallows()
|
||||
{
|
||||
@@ -757,6 +781,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
PurgeCatchUpInterval = TimeSpan.FromSeconds(1),
|
||||
};
|
||||
|
||||
/// <summary>Verifies that the retention purge stops at the per-tick row ceiling.</summary>
|
||||
[Fact]
|
||||
public void PurgeByRetention_StopsAtThePerTickCeiling()
|
||||
{
|
||||
@@ -773,6 +798,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(1500, GetEventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the retention purge with a ceiling converges across successive ticks.</summary>
|
||||
[Fact]
|
||||
public void PurgeByRetention_WithCeiling_ConvergesAcrossSuccessiveTicks()
|
||||
{
|
||||
@@ -792,6 +818,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(1, GetEventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an expired set that lands exactly on the ceiling is not counted as a ceiling hit.</summary>
|
||||
[Fact]
|
||||
public void PurgeByRetention_ExpiredSetLandingExactlyOnTheCeiling_IsNotACeilingHit()
|
||||
{
|
||||
@@ -806,10 +833,11 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(0, GetEventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a zero MaxPurgedRowsPerTick restores unbounded purges.</summary>
|
||||
[Fact]
|
||||
public void MaxPurgedRowsPerTick_Zero_RestoresUnboundedPurges()
|
||||
{
|
||||
// The documented escape hatch: 0 means unlimited, i.e. byte-for-byte the pre-row-34
|
||||
// The documented escape hatch: 0 means unlimited, i.e. byte-for-byte the pre-ceiling
|
||||
// behaviour, for an operator who would rather take one long burst than a paced one.
|
||||
InsertExpiredEvents(2500);
|
||||
|
||||
@@ -819,6 +847,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(0, GetEventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that hitting the ceiling logs the ceiling line and increments the counter.</summary>
|
||||
[Fact]
|
||||
public void RunPurge_HittingTheCeiling_LogsTheCeilingLineAndCountsIt()
|
||||
{
|
||||
@@ -838,6 +867,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(1, probe.Total);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that repeated ceiling hits rate-limit the log line but not the counter.</summary>
|
||||
[Fact]
|
||||
public void RunPurge_RepeatedCeilingHits_RateLimitTheLogLineButNotTheCounter()
|
||||
{
|
||||
@@ -855,6 +885,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.Equal(2, probe.Total);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the storage-cap purge shares the per-tick budget with the retention purge.</summary>
|
||||
[Fact]
|
||||
public void PurgeByStorageCap_SharesTheTickBudgetWithRetention()
|
||||
{
|
||||
@@ -875,6 +906,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
Assert.DoesNotContain(logger.Entries, e => e.Message.Contains("exceeds cap", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the storage-cap purge with a ceiling stops at the budget and asks for a catch-up tick.</summary>
|
||||
[Fact]
|
||||
public void PurgeByStorageCap_WithCeiling_StopsAtTheBudgetAndAsksForCatchUp()
|
||||
{
|
||||
@@ -904,6 +936,8 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
private readonly System.Diagnostics.Metrics.MeterListener _listener = new();
|
||||
private long _total;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="CounterProbe"/> class listening to the named instrument.</summary>
|
||||
/// <param name="instrumentName">The name of the counter instrument to listen to.</param>
|
||||
public CounterProbe(string instrumentName)
|
||||
{
|
||||
_listener.InstrumentPublished = (instrument, listener) =>
|
||||
@@ -919,6 +953,7 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
/// <summary>Measurements recorded since construction.</summary>
|
||||
public long Total => Interlocked.Read(ref _total);
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose() => _listener.Dispose();
|
||||
}
|
||||
|
||||
@@ -929,12 +964,27 @@ public class EventLogPurgeServiceTests : IDisposable
|
||||
/// </summary>
|
||||
private sealed class CapturingLogger : ILogger<EventLogPurgeService>
|
||||
{
|
||||
/// <summary>Gets the captured log entries in the order they were written.</summary>
|
||||
public List<(LogLevel Level, string Message)> Entries { get; } = [];
|
||||
|
||||
/// <summary>Starts no scope; the fake does not track scopes.</summary>
|
||||
/// <typeparam name="TState">The type of the scope state.</typeparam>
|
||||
/// <param name="state">The scope state, ignored.</param>
|
||||
/// <returns>No scope token; the fake does not track scopes.</returns>
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
/// <summary>Reports every log level as enabled.</summary>
|
||||
/// <param name="logLevel">The log level being queried.</param>
|
||||
/// <returns>Always <see langword="true"/>.</returns>
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <summary>Records the log entry for later assertions.</summary>
|
||||
/// <typeparam name="TState">The type of the log state.</typeparam>
|
||||
/// <param name="logLevel">The severity of the entry.</param>
|
||||
/// <param name="eventId">The event id of the entry.</param>
|
||||
/// <param name="state">The state object being logged.</param>
|
||||
/// <param name="exception">The exception associated with the entry, if any.</param>
|
||||
/// <param name="formatter">Formats the state and exception into the message text.</param>
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel,
|
||||
EventId eventId,
|
||||
|
||||
@@ -11,6 +11,7 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
private readonly TestLocalDb _localDb;
|
||||
private readonly string _dbPath;
|
||||
|
||||
/// <summary>Initializes the test class with a temporary LocalDb-backed event logger and query service.</summary>
|
||||
public EventLogQueryServiceTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_query_{Guid.NewGuid()}.db");
|
||||
@@ -27,6 +28,7 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
NullLogger<EventLogQueryService>.Instance);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources used by this instance.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_eventLogger.Dispose();
|
||||
@@ -66,6 +68,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
PageSize: pageSize,
|
||||
Timestamp: DateTimeOffset.UtcNow);
|
||||
|
||||
/// <summary>Verifies that a query with no filters returns all events.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_ReturnsAllEvents_WhenNoFilters()
|
||||
{
|
||||
@@ -78,6 +82,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.False(response.HasMore);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query filters events by event type.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_FiltersByEventType()
|
||||
{
|
||||
@@ -90,6 +96,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.All(response.Entries, e => Assert.Equal("script", e.EventType));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query filters events by severity.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_FiltersBySeverity()
|
||||
{
|
||||
@@ -102,6 +110,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.All(response.Entries, e => Assert.Equal("Error", e.Severity));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query filters events by instance id.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_FiltersByInstanceId()
|
||||
{
|
||||
@@ -114,6 +124,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.All(response.Entries, e => Assert.Equal("inst-1", e.InstanceId));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a keyword search matches the event message.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_KeywordSearch_MatchesMessage()
|
||||
{
|
||||
@@ -126,6 +138,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Contains("timeout", response.Entries[0].Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a keyword search matches the event source.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_KeywordSearch_MatchesSource()
|
||||
{
|
||||
@@ -138,6 +152,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Contains("AlarmActor", response.Entries[0].Source);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query combines multiple filters.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_CombinesMultipleFilters()
|
||||
{
|
||||
@@ -153,6 +169,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Equal("Script timeout", response.Entries[0].Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that pagination returns the requested page size.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_Pagination_ReturnsCorrectPageSize()
|
||||
{
|
||||
@@ -166,6 +184,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.NotNull(response.ContinuationToken);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the continuation token retrieves the next page correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_Pagination_ContinuationTokenWorksCorrectly()
|
||||
{
|
||||
@@ -198,6 +218,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Equal(5, allIds.Distinct().Count());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query filters events by time range.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_FiltersByTimeRange()
|
||||
{
|
||||
@@ -217,10 +239,12 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Equal(2, response.Entries.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a time range filter normalises non-UTC offsets correctly.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_FiltersByTimeRange_HandlesNonUtcOffset()
|
||||
{
|
||||
// SiteEventLogging-024 (re-opens -016): the store holds UTC ISO-8601 "o"
|
||||
// The store holds UTC ISO-8601 "o"
|
||||
// strings (always +00:00) and compares them lexicographically. If the
|
||||
// From/To bounds are stringified verbatim without UTC normalisation, a
|
||||
// non-UTC DateTimeOffset from a central client sorts wrongly against the
|
||||
@@ -253,6 +277,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Equal(new[] { "MIDDLE" }, response.Entries.Select(e => e.Source).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query with no matches returns an empty result.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_EmptyResult_WhenNoMatches()
|
||||
{
|
||||
@@ -266,6 +292,7 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Null(response.ContinuationToken);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query returns the correlation id of each event.</summary>
|
||||
[Fact]
|
||||
public void Query_ReturnsCorrelationId()
|
||||
{
|
||||
@@ -276,6 +303,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Equal("site-1", response.SiteId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a query returns every field of the event log entry.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_ReturnsAllEventLogEntryFields()
|
||||
{
|
||||
@@ -294,10 +323,12 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Equal("{\"stack\":\"trace\"}", entry.Details);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a keyword search treats an underscore as a literal rather than a wildcard.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_KeywordSearch_TreatsUnderscoreAsLiteral_NotWildcard()
|
||||
{
|
||||
// SiteEventLogging-013: a SQL LIKE '_' matches any single character. A
|
||||
// A SQL LIKE '_' matches any single character. A
|
||||
// keyword search for a literal underscore (common in identifiers such as
|
||||
// "store_and_forward") must NOT match strings that merely have any
|
||||
// character in that position.
|
||||
@@ -311,10 +342,12 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Equal("store_and_forward", response.Entries[0].Source);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a keyword search treats a percent sign as a literal rather than a wildcard.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_KeywordSearch_TreatsPercentAsLiteral_NotWildcard()
|
||||
{
|
||||
// SiteEventLogging-013: a SQL LIKE '%' matches any run of characters. A
|
||||
// A SQL LIKE '%' matches any run of characters. A
|
||||
// keyword search containing a literal '%' must match only the literal.
|
||||
await _eventLogger.LogEventAsync("script", "Warning", null, "ScriptActor:Run", "CPU at 90% utilisation");
|
||||
await _eventLogger.LogEventAsync("script", "Warning", null, "ScriptActor:Run", "CPU at 90 then 0 utilisation");
|
||||
@@ -326,6 +359,8 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
Assert.Contains("90% utilisation", response.Entries[0].Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a keyword search still matches a plain substring.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_KeywordSearch_StillMatchesPlainSubstring()
|
||||
{
|
||||
@@ -358,8 +393,10 @@ public class EventLogQueryServiceTests : IDisposable
|
||||
});
|
||||
}
|
||||
|
||||
// --- SiteEventLogging-017: PageSize hard upper bound ---
|
||||
// --- PageSize hard upper bound ---
|
||||
|
||||
/// <summary>Verifies that a requested page size is clamped to the maximum query page size.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Query_PageSize_IsClampedToMaxQueryPageSize()
|
||||
{
|
||||
|
||||
@@ -33,6 +33,8 @@ public class EventLogStorageCapScopeTests : IDisposable
|
||||
private readonly TestLocalDb _localDb;
|
||||
private readonly string _dbPath;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>EventLogStorageCapScopeTests</c> class.</summary>
|
||||
/// <param name="output">The output.</param>
|
||||
public EventLogStorageCapScopeTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
@@ -44,6 +46,7 @@ public class EventLogStorageCapScopeTests : IDisposable
|
||||
_localDb.Db);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this instance.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_eventLogger.Dispose();
|
||||
@@ -128,6 +131,7 @@ public class EventLogStorageCapScopeTests : IDisposable
|
||||
return (long)cmd.ExecuteScalar()!;
|
||||
});
|
||||
|
||||
/// <summary>Verifies that the estimated event-log size excludes other tables in the same database file.</summary>
|
||||
[Fact]
|
||||
public void EstimatedSize_ExcludesOtherTablesInTheSameFile()
|
||||
{
|
||||
@@ -154,6 +158,7 @@ public class EventLogStorageCapScopeTests : IDisposable
|
||||
Assert.Equal(eventsOnly, eventsAfterBallast);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the storage-cap purge does not trim an under-cap event log when other tables in the same file are large.</summary>
|
||||
[Fact]
|
||||
public void PurgeByStorageCap_WithLargeBallastInTheSameFile_DoesNotTrimAnUnderCapEventLog()
|
||||
{
|
||||
@@ -179,6 +184,7 @@ public class EventLogStorageCapScopeTests : IDisposable
|
||||
Assert.Equal(4000, EventCount());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the storage-cap purge still enforces the cap on events without emptying the table when other tables in the same file are large.</summary>
|
||||
[Fact]
|
||||
public void PurgeByStorageCap_WithLargeBallastInTheSameFile_StillEnforcesTheCapOnEventsAndDoesNotEmptyTheTable()
|
||||
{
|
||||
@@ -217,6 +223,7 @@ public class EventLogStorageCapScopeTests : IDisposable
|
||||
|
||||
// ── size-model accuracy ──
|
||||
|
||||
/// <summary>Gets the workloads value.</summary>
|
||||
public static TheoryData<string, int, int, double> Workloads => new()
|
||||
{
|
||||
// name, rows, details length (0 = no details column), details on every row
|
||||
@@ -225,6 +232,11 @@ public class EventLogStorageCapScopeTests : IDisposable
|
||||
{ "wide rows, 2KB details", 3000, 2048, 1.0 },
|
||||
};
|
||||
|
||||
/// <summary>Verifies that the site-events size estimate tracks the real on-disk footprint across workloads.</summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="rows">The rows.</param>
|
||||
/// <param name="detailsLength">The details length.</param>
|
||||
/// <param name="detailsFraction">The details fraction.</param>
|
||||
[Theory]
|
||||
[MemberData(nameof(Workloads))]
|
||||
public void EstimateSiteEventsSize_TracksTheRealFootprint(string name, int rows, int detailsLength, double detailsFraction)
|
||||
@@ -292,13 +304,16 @@ public class EventLogStorageCapScopeTests : IDisposable
|
||||
long? frozenRowCount)
|
||||
: EventLogPurgeService(eventLogger, options, logger)
|
||||
{
|
||||
/// <inheritdoc />
|
||||
internal override EventLogPurgeService.SiteEventsSizeEstimate EstimateSiteEventsSize() =>
|
||||
estimate ?? base.EstimateSiteEventsSize();
|
||||
|
||||
/// <inheritdoc />
|
||||
internal override long GetSiteEventsRowCount() =>
|
||||
frozenRowCount ?? base.GetSiteEventsRowCount();
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the storage-cap purge deletes nothing and logs a warning when the row width cannot be sampled.</summary>
|
||||
[Fact]
|
||||
public void PurgeByStorageCap_WhenRowWidthCannotBeSampled_DeletesNothingAndWarns()
|
||||
{
|
||||
@@ -322,6 +337,7 @@ public class EventLogStorageCapScopeTests : IDisposable
|
||||
e.Level == LogLevel.Warning && e.Message.Contains("row width could not be sampled", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the storage-cap purge stops rather than emptying the log when the table stops shrinking.</summary>
|
||||
[Fact]
|
||||
public void PurgeByStorageCap_WhenTheTableStopsShrinking_StopsRatherThanEmptyingTheLog()
|
||||
{
|
||||
@@ -351,12 +367,27 @@ public class EventLogStorageCapScopeTests : IDisposable
|
||||
|
||||
private sealed class CapturingLogger : ILogger<EventLogPurgeService>
|
||||
{
|
||||
/// <summary>Gets the entries value.</summary>
|
||||
public List<LogEntry> Entries { get; } = [];
|
||||
|
||||
/// <summary>Returns no scope; the fake does not track scopes.</summary>
|
||||
/// <typeparam name="TState">The type of the scope state.</typeparam>
|
||||
/// <param name="state">The scope state (ignored).</param>
|
||||
/// <returns>Always null.</returns>
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
/// <summary>Reports every log level as enabled.</summary>
|
||||
/// <param name="logLevel">The log level to check.</param>
|
||||
/// <returns>Always true.</returns>
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <summary>Records the log entry for later assertions.</summary>
|
||||
/// <typeparam name="TState">The type of the log state.</typeparam>
|
||||
/// <param name="logLevel">The severity of the entry.</param>
|
||||
/// <param name="eventId">The event identifier.</param>
|
||||
/// <param name="state">The state object to format.</param>
|
||||
/// <param name="exception">The exception associated with the entry, if any.</param>
|
||||
/// <param name="formatter">Formats the state and exception into the message.</param>
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter) =>
|
||||
|
||||
@@ -5,7 +5,7 @@ using Microsoft.Extensions.Options;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for SiteEventLogging-006: the schema must index the columns the
|
||||
/// Regression tests: the schema must index the columns the
|
||||
/// query service filters on so common queries do not full-scan a 1 GB database.
|
||||
/// </summary>
|
||||
public class SchemaIndexTests : IDisposable
|
||||
@@ -15,6 +15,7 @@ public class SchemaIndexTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly TestLocalDb _localDb;
|
||||
|
||||
/// <summary>Initializes a new instance of the <c>SchemaIndexTests</c> class.</summary>
|
||||
public SchemaIndexTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_index_{Guid.NewGuid()}.db");
|
||||
@@ -26,6 +27,7 @@ public class SchemaIndexTests : IDisposable
|
||||
_verifyConnection.Open();
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources used by this instance.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_verifyConnection.Dispose();
|
||||
@@ -34,6 +36,7 @@ public class SchemaIndexTests : IDisposable
|
||||
TestLocalDb.DeleteFiles(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that schema: has index on severity.</summary>
|
||||
[Fact]
|
||||
public void Schema_HasIndexOnSeverity()
|
||||
{
|
||||
@@ -47,6 +50,8 @@ public class SchemaIndexTests : IDisposable
|
||||
Assert.Contains("idx_events_severity", indexes);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that severity filtered query, when uses index, not full scan.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task SeverityFilteredQuery_UsesIndex_NotFullScan()
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ using ZB.MOM.WW.LocalDb;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for SiteEventLogging-007: the purge and query services must not
|
||||
/// Regression tests: the purge and query services must not
|
||||
/// downcast an injected <see cref="ISiteEventLogger"/> to the concrete type. They now
|
||||
/// depend on the concrete <see cref="SiteEventLogger"/> directly, and DI must resolve
|
||||
/// the recorder, query service, and purge service to a single shared instance.
|
||||
@@ -17,17 +17,20 @@ public class ServiceWiringTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private ServiceProvider? _provider;
|
||||
|
||||
/// <summary>Initializes the test class with a service provider backed by a temporary database.</summary>
|
||||
public ServiceWiringTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_wiring_{Guid.NewGuid()}.db");
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_provider?.Dispose();
|
||||
TestLocalDb.DeleteFiles(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that registering site event logging resolves all services and shares one recorder instance.</summary>
|
||||
[Fact]
|
||||
public void AddSiteEventLogging_ResolvesAllServices_SharingOneRecorderInstance()
|
||||
{
|
||||
@@ -61,6 +64,7 @@ public class ServiceWiringTests : IDisposable
|
||||
Assert.Contains(hosted, h => h is EventLogPurgeService);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the purge and query services accept the concrete recorder without downcasting.</summary>
|
||||
[Fact]
|
||||
public void PurgeAndQueryServices_AcceptConcreteRecorder_WithoutDowncast()
|
||||
{
|
||||
|
||||
+8
@@ -14,6 +14,7 @@ public class SiteEventLogOptionsValidatorTests
|
||||
private static ValidateOptionsResult Validate(SiteEventLogOptions options) =>
|
||||
new SiteEventLogOptionsValidator().Validate(Options.DefaultName, options);
|
||||
|
||||
/// <summary>Verifies that the default options are valid.</summary>
|
||||
[Fact]
|
||||
public void DefaultOptions_AreValid()
|
||||
{
|
||||
@@ -21,6 +22,7 @@ public class SiteEventLogOptionsValidatorTests
|
||||
Assert.True(result.Succeeded, result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that zero retention days is rejected.</summary>
|
||||
[Fact]
|
||||
public void ZeroRetentionDays_IsRejected()
|
||||
{
|
||||
@@ -30,6 +32,7 @@ public class SiteEventLogOptionsValidatorTests
|
||||
Assert.Contains("RetentionDays", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a zero purge interval is rejected.</summary>
|
||||
[Fact]
|
||||
public void ZeroPurgeInterval_IsRejected()
|
||||
{
|
||||
@@ -39,6 +42,7 @@ public class SiteEventLogOptionsValidatorTests
|
||||
Assert.Contains("PurgeInterval", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a maximum query page size below the default page size is rejected.</summary>
|
||||
[Fact]
|
||||
public void MaxQueryPageSize_BelowQueryPageSize_IsRejected()
|
||||
{
|
||||
@@ -50,6 +54,7 @@ public class SiteEventLogOptionsValidatorTests
|
||||
|
||||
// ── Register row 34: the per-tick purge ceiling ──
|
||||
|
||||
/// <summary>Verifies that a negative maximum of purged rows per tick is rejected.</summary>
|
||||
[Fact]
|
||||
public void NegativeMaxPurgedRowsPerTick_IsRejected()
|
||||
{
|
||||
@@ -61,6 +66,7 @@ public class SiteEventLogOptionsValidatorTests
|
||||
Assert.Contains("MaxPurgedRowsPerTick", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a zero maximum of purged rows per tick (unlimited) is accepted.</summary>
|
||||
[Fact]
|
||||
public void ZeroMaxPurgedRowsPerTick_IsAccepted()
|
||||
{
|
||||
@@ -70,6 +76,7 @@ public class SiteEventLogOptionsValidatorTests
|
||||
Assert.True(result.Succeeded, result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a zero purge catch-up interval is rejected.</summary>
|
||||
[Fact]
|
||||
public void ZeroPurgeCatchUpInterval_IsRejected()
|
||||
{
|
||||
@@ -80,6 +87,7 @@ public class SiteEventLogOptionsValidatorTests
|
||||
Assert.Contains("PurgeCatchUpInterval", result.FailureMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a purge catch-up interval longer than the purge interval is rejected.</summary>
|
||||
[Fact]
|
||||
public void PurgeCatchUpInterval_LongerThanPurgeInterval_IsRejected()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Microsoft.Data.Sqlite;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// LocalDb Phase 1 (Task 2) — the <c>site_events</c> DDL is extracted out of
|
||||
/// The <c>site_events</c> DDL is extracted out of
|
||||
/// <see cref="SiteEventLogger"/> so the Host's <c>AddZbLocalDb</c> onReady callback can
|
||||
/// create it in the consolidated site database.
|
||||
/// <para>
|
||||
@@ -39,6 +39,7 @@ public class SiteEventLogSchemaTests
|
||||
return cols;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that applying the schema creates the table with the expected columns.</summary>
|
||||
[Fact]
|
||||
public void Apply_CreatesTableWithExpectedColumns()
|
||||
{
|
||||
@@ -51,6 +52,7 @@ public class SiteEventLogSchemaTests
|
||||
TableInfo(conn, "site_events").Select(c => c.Name));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the id column is a text primary key rather than an autoincrement integer.</summary>
|
||||
[Fact]
|
||||
public void Apply_IdIsTextPrimaryKey_NotAutoincrement()
|
||||
{
|
||||
@@ -71,6 +73,7 @@ public class SiteEventLogSchemaTests
|
||||
Assert.Equal(0L, Convert.ToInt64(cmd.ExecuteScalar()));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that inserts that omit the text id are rejected.</summary>
|
||||
[Fact]
|
||||
public void Apply_RejectsIntegerIdInserts()
|
||||
{
|
||||
@@ -79,7 +82,7 @@ public class SiteEventLogSchemaTests
|
||||
SiteEventLogSchema.Apply(conn);
|
||||
|
||||
// A TEXT PK with no AUTOINCREMENT means an INSERT omitting id is an error,
|
||||
// which is what forces the writer to mint one (Task 5a).
|
||||
// which is what forces the writer to mint one.
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = """
|
||||
INSERT INTO site_events (timestamp, event_type, severity, source, message)
|
||||
@@ -88,6 +91,7 @@ public class SiteEventLogSchemaTests
|
||||
Assert.Throws<SqliteException>(() => cmd.ExecuteNonQuery());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the schema has no blob columns.</summary>
|
||||
[Fact]
|
||||
public void Apply_HasNoBlobColumns()
|
||||
{
|
||||
@@ -100,6 +104,7 @@ public class SiteEventLogSchemaTests
|
||||
c => c.Type.Contains("BLOB", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that applying the schema creates the expected indexes.</summary>
|
||||
[Fact]
|
||||
public void Apply_CreatesExpectedIndexes()
|
||||
{
|
||||
@@ -120,6 +125,7 @@ public class SiteEventLogSchemaTests
|
||||
indexes);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that applying the schema repeatedly is safe.</summary>
|
||||
[Fact]
|
||||
public void Apply_IsIdempotent()
|
||||
{
|
||||
|
||||
@@ -6,8 +6,8 @@ using Microsoft.Extensions.Options;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for SiteEventLogging-005 (synchronous I/O on caller thread)
|
||||
/// and SiteEventLogging-008 (write failures silently swallowed).
|
||||
/// Regression tests for non-blocking logging (synchronous I/O on the caller thread)
|
||||
/// and surfaced write failures (failures silently swallowed).
|
||||
/// </summary>
|
||||
public class SiteEventLoggerAsyncTests : IDisposable
|
||||
{
|
||||
@@ -15,6 +15,7 @@ public class SiteEventLoggerAsyncTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly TestLocalDb _localDb;
|
||||
|
||||
/// <summary>Initializes the test class with a temporary LocalDb-backed event logger.</summary>
|
||||
public SiteEventLoggerAsyncTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_async_{Guid.NewGuid()}.db");
|
||||
@@ -23,6 +24,7 @@ public class SiteEventLoggerAsyncTests : IDisposable
|
||||
_logger = new SiteEventLogger(options, NullLogger<SiteEventLogger>.Instance, _localDb.Db);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.Dispose();
|
||||
@@ -30,10 +32,12 @@ public class SiteEventLoggerAsyncTests : IDisposable
|
||||
TestLocalDb.DeleteFiles(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that logging an event does not block the caller when the write is slow.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_DoesNotBlockCaller_WhenWriteIsSlow()
|
||||
{
|
||||
// SiteEventLogging-005: LogEventAsync must not perform the SQLite write
|
||||
// LogEventAsync must not perform the SQLite write
|
||||
// inline on the caller's thread. We hold the recorder busy on a long
|
||||
// database operation from one thread, then time how long it takes the
|
||||
// caller of LogEventAsync to get control back. If recording is offloaded
|
||||
@@ -71,6 +75,8 @@ public class SiteEventLoggerAsyncTests : IDisposable
|
||||
await recordTask.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the logging task completes only after the event is persisted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_TaskCompletes_AfterEventIsPersisted()
|
||||
{
|
||||
@@ -86,10 +92,12 @@ public class SiteEventLoggerAsyncTests : IDisposable
|
||||
Assert.Equal(1, count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a write error faults the logging task and increments the failure count.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_FaultsTask_AndCountsFailure_OnWriteError()
|
||||
{
|
||||
// SiteEventLogging-008: a write failure must not be silently swallowed.
|
||||
// A write failure must not be silently swallowed.
|
||||
// The returned Task must fault and the failure counter must increment so
|
||||
// Health Monitoring can surface a logging outage.
|
||||
using var failingConnection = new SqliteConnection("Data Source=:memory:");
|
||||
|
||||
@@ -4,7 +4,7 @@ using Microsoft.Extensions.Options;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// WP1.7 (arch-review remediation, 2026-08-14): <see cref="SiteEventLogger"/>'s
|
||||
/// The <see cref="SiteEventLogger"/>'s
|
||||
/// background writer loop must drain its bounded channel into batched transactions
|
||||
/// (up to <see cref="SiteEventLogger.WriteBatchSize"/> events per commit) instead of
|
||||
/// committing one transaction per event — mirroring the reference shape in
|
||||
@@ -17,6 +17,7 @@ public class SiteEventLoggerBatchingTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly TestLocalDb _localDb;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SiteEventLoggerBatchingTests"/> class with a temporary LocalDb file.</summary>
|
||||
public SiteEventLoggerBatchingTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_batch_{Guid.NewGuid()}.db");
|
||||
@@ -25,6 +26,7 @@ public class SiteEventLoggerBatchingTests : IDisposable
|
||||
_logger = new SiteEventLogger(options, NullLogger<SiteEventLogger>.Instance, _localDb.Db);
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this test class.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.Dispose();
|
||||
@@ -32,6 +34,8 @@ public class SiteEventLoggerBatchingTests : IDisposable
|
||||
TestLocalDb.DeleteFiles(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that logging a single event commits exactly one batch.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_SingleEvent_CommitsOneBatch()
|
||||
{
|
||||
@@ -40,6 +44,8 @@ public class SiteEventLoggerBatchingTests : IDisposable
|
||||
Assert.Equal(1, _logger.CommittedBatchCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a burst of queued events commits far fewer batches than events.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ProcessWriteQueue_BurstOfQueuedEvents_CommitsFarFewerBatchesThanEvents()
|
||||
{
|
||||
@@ -102,6 +108,8 @@ public class SiteEventLoggerBatchingTests : IDisposable
|
||||
Assert.Equal(burstCount, count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a batch failure faults every queued event so none is silently dropped.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ProcessWriteQueue_BatchFailure_FaultsEveryQueuedEvent_NoneSilentlyDropped()
|
||||
{
|
||||
@@ -156,6 +164,8 @@ public class SiteEventLoggerBatchingTests : IDisposable
|
||||
$"Expected FailedWriteCount to account for all {burstCount} events in the rolled-back batch, got {_logger.FailedWriteCount}.");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a poison row in the middle of a batch fails alone while the other rows are written.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ProcessWriteQueue_PoisonRowMidBatch_OtherRowsLand_AndOnlyThePoisonRowIsCountedFailed()
|
||||
{
|
||||
@@ -223,6 +233,8 @@ public class SiteEventLoggerBatchingTests : IDisposable
|
||||
Assert.Equal(burstCount - 1, landed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing the logger persists events that are still queued at shutdown.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Dispose_PersistsEventsStillQueuedAtShutdown()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ public class SiteEventLoggerTests : IDisposable
|
||||
private readonly string _dbPath;
|
||||
private readonly TestLocalDb _localDb;
|
||||
|
||||
/// <summary>Initializes the test class with a temporary LocalDb-backed logger.</summary>
|
||||
public SiteEventLoggerTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"test_events_{Guid.NewGuid()}.db");
|
||||
@@ -23,6 +24,7 @@ public class SiteEventLoggerTests : IDisposable
|
||||
_verifyConnection.Open();
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources held by this instance.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_verifyConnection.Dispose();
|
||||
@@ -31,6 +33,8 @@ public class SiteEventLoggerTests : IDisposable
|
||||
TestLocalDb.DeleteFiles(_dbPath);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that logging an event inserts a record.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_InsertsRecord()
|
||||
{
|
||||
@@ -42,6 +46,8 @@ public class SiteEventLoggerTests : IDisposable
|
||||
Assert.Equal(1, count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that logging an event stores all of its fields.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_StoresAllFields()
|
||||
{
|
||||
@@ -59,6 +65,8 @@ public class SiteEventLoggerTests : IDisposable
|
||||
Assert.Equal("{\"value\":95}", reader.GetString(5));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the nullable fields may be left empty.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_NullableFieldsAllowed()
|
||||
{
|
||||
@@ -72,6 +80,8 @@ public class SiteEventLoggerTests : IDisposable
|
||||
Assert.True(reader.IsDBNull(1));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the stored timestamp is an ISO 8601 UTC value.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_StoresIso8601UtcTimestamp()
|
||||
{
|
||||
@@ -84,6 +94,8 @@ public class SiteEventLoggerTests : IDisposable
|
||||
Assert.Equal(TimeSpan.Zero, parsed.Offset);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty event type throws.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_ThrowsOnEmptyEventType()
|
||||
{
|
||||
@@ -91,6 +103,8 @@ public class SiteEventLoggerTests : IDisposable
|
||||
_logger.LogEventAsync("", "Info", null, "Source", "Message"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty severity throws.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_ThrowsOnEmptySeverity()
|
||||
{
|
||||
@@ -98,6 +112,8 @@ public class SiteEventLoggerTests : IDisposable
|
||||
_logger.LogEventAsync("script", "", null, "Source", "Message"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty source throws.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_ThrowsOnEmptySource()
|
||||
{
|
||||
@@ -105,6 +121,8 @@ public class SiteEventLoggerTests : IDisposable
|
||||
_logger.LogEventAsync("script", "Info", null, "", "Message"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an empty message throws.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_ThrowsOnEmptyMessage()
|
||||
{
|
||||
@@ -112,6 +130,8 @@ public class SiteEventLoggerTests : IDisposable
|
||||
_logger.LogEventAsync("script", "Info", null, "Source", ""));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that multiple events each get a distinct application-minted id.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task LogEventAsync_MultipleEvents_GetDistinctApplicationMintedIds()
|
||||
{
|
||||
@@ -139,6 +159,8 @@ public class SiteEventLoggerTests : IDisposable
|
||||
$"Event id '{id}' is not a GUID in 'N' format."));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that all event types are accepted.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task AllEventTypes_Accepted()
|
||||
{
|
||||
@@ -154,8 +176,11 @@ public class SiteEventLoggerTests : IDisposable
|
||||
Assert.Equal(6, count);
|
||||
}
|
||||
|
||||
// --- SiteEventLogging-020: severity validation against the closed set ---
|
||||
// --- Severity validation against the closed set ---
|
||||
|
||||
/// <summary>Verifies that an unknown severity throws.</summary>
|
||||
/// <param name="badSeverity">The bad severity.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("info")] // wrong casing
|
||||
[InlineData("warn")] // abbreviation
|
||||
@@ -170,6 +195,9 @@ public class SiteEventLoggerTests : IDisposable
|
||||
Assert.Contains("Info, Warning, Error", ex.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that every documented severity is accepted.</summary>
|
||||
/// <param name="severity">The severity.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData("Info")]
|
||||
[InlineData("Warning")]
|
||||
|
||||
@@ -43,6 +43,7 @@ public class SiteEventsSchemaCalibrationTests
|
||||
return values;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the modelled columns exactly match the sampled octet-length sum.</summary>
|
||||
[Fact]
|
||||
public void Columns_MatchTheSampledOctetLengthSum_Exactly()
|
||||
{
|
||||
@@ -59,6 +60,7 @@ public class SiteEventsSchemaCalibrationTests
|
||||
$"{RecalibrationNotice} Actual columns: {string.Join(", ", columns)}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the table has exactly the four indexes the model charges for, plus the primary-key autoindex.</summary>
|
||||
[Fact]
|
||||
public void Indexes_AreExactlyTheFourTheModelCharges_PlusThePkAutoindex()
|
||||
{
|
||||
@@ -82,6 +84,9 @@ public class SiteEventsSchemaCalibrationTests
|
||||
$"{RecalibrationNotice} Actual indexes: {string.Join(", ", indexes)}");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that each index covers exactly the column the sampler sums.</summary>
|
||||
/// <param name="indexName">The index name.</param>
|
||||
/// <param name="expectedColumn">The column the index is expected to cover.</param>
|
||||
[Theory]
|
||||
[InlineData("idx_events_timestamp", "timestamp")]
|
||||
[InlineData("idx_events_type", "event_type")]
|
||||
|
||||
@@ -23,6 +23,7 @@ public class SqliteCapabilityTests
|
||||
return connection;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the bundled SQLite build has no dbstat table, so the per-table page count has to be modelled.</summary>
|
||||
[Fact]
|
||||
public void DbStat_IsNotAvailable_SoThePerTablePageCountMustBeModelled()
|
||||
{
|
||||
@@ -64,6 +65,7 @@ public class SqliteCapabilityTests
|
||||
Assert.Contains("dbstat", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that octet_length is available, so row width is sampled in bytes rather than characters.</summary>
|
||||
[Fact]
|
||||
public void OctetLength_IsAvailable_SoRowWidthIsSampledInBytesNotCharacters()
|
||||
{
|
||||
|
||||
@@ -38,6 +38,8 @@ public sealed class TestLocalDb : IDisposable
|
||||
public ILocalDb Db { get; }
|
||||
|
||||
/// <summary>Opens a local database at <paramref name="databasePath"/>.</summary>
|
||||
/// <param name="databasePath">The filesystem path of the SQLite database file.</param>
|
||||
/// <returns>A wrapper owning the opened database and its service provider.</returns>
|
||||
public static TestLocalDb Create(string databasePath)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
@@ -58,6 +60,7 @@ public sealed class TestLocalDb : IDisposable
|
||||
/// Deletes <paramref name="databasePath"/> and its WAL sidecars. Call only after the
|
||||
/// owning <see cref="TestLocalDb"/> is disposed — the master connection anchors the WAL.
|
||||
/// </summary>
|
||||
/// <param name="databasePath">The filesystem path of the SQLite database file whose files are deleted.</param>
|
||||
public static void DeleteFiles(string databasePath)
|
||||
{
|
||||
foreach (var suffix in new[] { "", "-wal", "-shm" })
|
||||
@@ -66,5 +69,6 @@ public sealed class TestLocalDb : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Disposes the underlying service provider.</summary>
|
||||
public void Dispose() => _provider.Dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user