using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Conformance; /// /// 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. /// public interface IConformanceBackend : IAsyncDisposable { /// Storage timestamp resolution in .NET ticks: 1 for SQL Server (100 ns), 10 for QuestDB (µs). long ResolutionTicks { get; } /// Truncates a timestamp to the backend's storage resolution (UTC kind). Every timestamp assertion goes through it. /// The timestamp. /// The value the backend stores/serves for . DateTime Round(DateTime t) => new(t.Ticks - t.Ticks % ResolutionTicks, DateTimeKind.Utc); /// A fresh data source (fresh catalog cache) over this backend's store. /// Its "now"; null means the system clock. IHistorianDataSource CreateDataSource(TimeProvider? clock = null); /// A fresh data source whose time-weighted raw-row cap (ServerHistorian:MaxTimeWeightedRawRows) is . /// The cap (<= 0 = unbounded). IHistorianDataSource CreateDataSource(int maxTimeWeightedRawRows); /// A fresh live-value writer over this backend's store. /// The clock used to stamp values that carry no timestamp; null means the system clock. IHistorianValueWriter CreateValueWriter(Func? utcNow = null); /// A fresh history updater over this backend's store. IHistorianDataUpdater CreateUpdater(); /// /// 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. /// /// The tag name. /// The rows to store. Task SeedAsync(string tag, params (DateTime T, HistorianValue V, uint Status)[] samples); /// /// Writes historical events directly in the backend's event table (never through the alarm writer): event id, /// source and time as given (rounded to the storage resolution), severity 500, received time = event time. /// /// The events to store. Task SeedEventsAsync(params (DateTime T, string Id, string Source)[] events); /// /// 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). /// /// The tag name. Task> ReadBackAsync(string tag); /// Whether a tag with exactly this name exists in storage (no truncation of long names). /// The tag name. Task TagExistsAsync(string tag); /// The data type the backend recorded for a tag, or null when the tag does not exist. /// The tag name. Task TagDataTypeAsync(string tag); } /// /// Base of every conformance class. A backend's test project subclasses each abstract suite class once, /// supplying , which must call its fixture's skip gate so the tests report Skipped /// (not Failed) when the backend is unavailable. /// public abstract class ConformanceBase : IAsyncLifetime { /// Good. protected const uint Good = ConformanceData.Good; /// Bad (generic). protected const uint Bad = ConformanceData.Bad; /// BadNoCommunication. protected const uint BadNoCommunication = ConformanceData.BadNoCommunication; /// Uncertain (generic). protected const uint Uncertain = ConformanceData.Uncertain; /// The backend under test for the current test. protected IConformanceBackend B { get; private set; } = null!; /// Creates this test's isolated backend; calls the fixture's skip gate first. protected abstract Task CreateBackendAsync(); /// T0 + seconds. protected static DateTime At(double s) => ConformanceData.At(s); /// public async ValueTask InitializeAsync() => B = await CreateBackendAsync(); /// public async ValueTask DisposeAsync() { if (B is not null) await B.DisposeAsync(); } }