test(historian): backend conformance suite (shared source), run against SQL Server
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Conformance;
|
||||
|
||||
/// <summary>
|
||||
/// One historian backend under test. The conformance suite talks to a backend only through this contract
|
||||
/// (plus Core.Abstractions), so the same hand-computed expectations run against SQL Server and QuestDB.
|
||||
/// Each instance owns an isolated store (a fresh schema / table set) so tests never see each other's data.
|
||||
/// </summary>
|
||||
public interface IConformanceBackend : IAsyncDisposable
|
||||
{
|
||||
/// <summary>Storage timestamp resolution in .NET ticks: 1 for SQL Server (100 ns), 10 for QuestDB (µs).</summary>
|
||||
long ResolutionTicks { get; }
|
||||
|
||||
/// <summary>Truncates a timestamp to the backend's storage resolution (UTC kind). Every timestamp assertion goes through it.</summary>
|
||||
/// <param name="t">The timestamp.</param>
|
||||
/// <returns>The value the backend stores/serves for <paramref name="t"/>.</returns>
|
||||
DateTime Round(DateTime t) => new(t.Ticks - t.Ticks % ResolutionTicks, DateTimeKind.Utc);
|
||||
|
||||
/// <summary>A fresh data source (fresh catalog cache) over this backend's store.</summary>
|
||||
/// <param name="clock">Its "now"; null means the system clock.</param>
|
||||
IHistorianDataSource CreateDataSource(TimeProvider? clock = null);
|
||||
|
||||
/// <summary>A fresh live-value writer over this backend's store.</summary>
|
||||
/// <param name="utcNow">The clock used to stamp values that carry no timestamp; null means the system clock.</param>
|
||||
IHistorianValueWriter CreateValueWriter(Func<DateTime>? utcNow = null);
|
||||
|
||||
/// <summary>A fresh history updater over this backend's store.</summary>
|
||||
IHistorianDataUpdater CreateUpdater();
|
||||
|
||||
/// <summary>
|
||||
/// Writes rows directly in the backend's schema (never through the writer or updater), status verbatim, creating the
|
||||
/// tag if needed. The stored server timestamp of each row is its source time + 1 s, so tests can tell them apart.
|
||||
/// </summary>
|
||||
/// <param name="tag">The tag name.</param>
|
||||
/// <param name="samples">The rows to store.</param>
|
||||
Task SeedAsync(string tag, params (DateTime T, HistorianValue V, uint Status)[] samples);
|
||||
|
||||
/// <summary>
|
||||
/// Live (non-deleted) rows for a tag, ascending by time, read directly from storage. The status is returned
|
||||
/// VERBATIM as stored — there is no read-side clamp (an Empty row stored non-Bad reads back non-Bad here).
|
||||
/// </summary>
|
||||
/// <param name="tag">The tag name.</param>
|
||||
Task<IReadOnlyList<(DateTime T, HistorianValue V, uint Status)>> ReadBackAsync(string tag);
|
||||
|
||||
/// <summary>Whether a tag with exactly this name exists in storage (no truncation of long names).</summary>
|
||||
/// <param name="tag">The tag name.</param>
|
||||
Task<bool> TagExistsAsync(string tag);
|
||||
|
||||
/// <summary>The data type the backend recorded for a tag, or null when the tag does not exist.</summary>
|
||||
/// <param name="tag">The tag name.</param>
|
||||
Task<DriverDataType?> TagDataTypeAsync(string tag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base of every conformance class. A backend's test project subclasses each abstract suite class once,
|
||||
/// supplying <see cref="CreateBackendAsync"/>, which must call its fixture's skip gate so the tests report Skipped
|
||||
/// (not Failed) when the backend is unavailable.
|
||||
/// </summary>
|
||||
public abstract class ConformanceBase : IAsyncLifetime
|
||||
{
|
||||
/// <summary>Good.</summary>
|
||||
protected const uint Good = ConformanceData.Good;
|
||||
/// <summary>Bad (generic).</summary>
|
||||
protected const uint Bad = ConformanceData.Bad;
|
||||
/// <summary>BadNoCommunication.</summary>
|
||||
protected const uint BadNoCommunication = ConformanceData.BadNoCommunication;
|
||||
/// <summary>Uncertain (generic).</summary>
|
||||
protected const uint Uncertain = ConformanceData.Uncertain;
|
||||
|
||||
/// <summary>The backend under test for the current test.</summary>
|
||||
protected IConformanceBackend B { get; private set; } = null!;
|
||||
|
||||
/// <summary>Creates this test's isolated backend; calls the fixture's skip gate first.</summary>
|
||||
protected abstract Task<IConformanceBackend> CreateBackendAsync();
|
||||
|
||||
/// <summary>T0 + <paramref name="s"/> seconds.</summary>
|
||||
protected static DateTime At(double s) => ConformanceData.At(s);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask InitializeAsync() => B = await CreateBackendAsync();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (B is not null) await B.DisposeAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user