148 lines
5.8 KiB
C#
148 lines
5.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using MessagePack;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Galaxy;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Historian;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.MxAccess;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Sta;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests;
|
|
|
|
[Trait("Category", "Unit")]
|
|
public sealed class HistoryReadAtTimeTests
|
|
{
|
|
private static MxAccessGalaxyBackend BuildBackend(IHistorianDataSource? historian, StaPump pump) =>
|
|
new(
|
|
new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }),
|
|
new MxAccessClient(pump, new MxProxyAdapter(), "attime-test"),
|
|
historian);
|
|
|
|
[Fact]
|
|
public async Task Returns_disabled_error_when_no_historian_configured()
|
|
{
|
|
using var pump = new StaPump("Test.Sta");
|
|
await pump.WaitForStartedAsync();
|
|
using var backend = BuildBackend(null, pump);
|
|
|
|
var resp = await backend.HistoryReadAtTimeAsync(new HistoryReadAtTimeRequest
|
|
{
|
|
TagReference = "T",
|
|
TimestampsUtcUnixMs = new[] { 1L, 2L },
|
|
}, CancellationToken.None);
|
|
|
|
resp.Success.ShouldBeFalse();
|
|
resp.Error.ShouldContain("Historian disabled");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Empty_timestamp_list_short_circuits_to_success_with_no_values()
|
|
{
|
|
using var pump = new StaPump("Test.Sta");
|
|
await pump.WaitForStartedAsync();
|
|
var fake = new FakeHistorian();
|
|
using var backend = BuildBackend(fake, pump);
|
|
|
|
var resp = await backend.HistoryReadAtTimeAsync(new HistoryReadAtTimeRequest
|
|
{
|
|
TagReference = "T",
|
|
TimestampsUtcUnixMs = Array.Empty<long>(),
|
|
}, CancellationToken.None);
|
|
|
|
resp.Success.ShouldBeTrue();
|
|
resp.Values.ShouldBeEmpty();
|
|
fake.Calls.ShouldBe(0); // no round-trip to SDK for empty timestamp list
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Timestamps_survive_Unix_ms_round_trip_to_DateTime()
|
|
{
|
|
using var pump = new StaPump("Test.Sta");
|
|
await pump.WaitForStartedAsync();
|
|
var t1 = new DateTime(2026, 4, 18, 10, 0, 0, DateTimeKind.Utc);
|
|
var t2 = new DateTime(2026, 4, 18, 10, 5, 0, DateTimeKind.Utc);
|
|
var fake = new FakeHistorian(
|
|
new HistorianSample { Value = 100.0, Quality = 192, TimestampUtc = t1 },
|
|
new HistorianSample { Value = 101.5, Quality = 192, TimestampUtc = t2 });
|
|
using var backend = BuildBackend(fake, pump);
|
|
|
|
var resp = await backend.HistoryReadAtTimeAsync(new HistoryReadAtTimeRequest
|
|
{
|
|
TagReference = "TankLevel",
|
|
TimestampsUtcUnixMs = new[]
|
|
{
|
|
new DateTimeOffset(t1, TimeSpan.Zero).ToUnixTimeMilliseconds(),
|
|
new DateTimeOffset(t2, TimeSpan.Zero).ToUnixTimeMilliseconds(),
|
|
},
|
|
}, CancellationToken.None);
|
|
|
|
resp.Success.ShouldBeTrue();
|
|
resp.Values.Length.ShouldBe(2);
|
|
resp.Values[0].SourceTimestampUtcUnixMs.ShouldBe(new DateTimeOffset(t1, TimeSpan.Zero).ToUnixTimeMilliseconds());
|
|
resp.Values[0].StatusCode.ShouldBe(0u); // Good (quality 192)
|
|
MessagePackSerializer.Deserialize<double>(resp.Values[0].ValueBytes!).ShouldBe(100.0);
|
|
|
|
fake.Calls.ShouldBe(1);
|
|
fake.LastTimestamps.Length.ShouldBe(2);
|
|
fake.LastTimestamps[0].ShouldBe(t1);
|
|
fake.LastTimestamps[1].ShouldBe(t2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Missing_sample_maps_to_Bad_category()
|
|
{
|
|
using var pump = new StaPump("Test.Sta");
|
|
await pump.WaitForStartedAsync();
|
|
// Quality=0 means no sample at that timestamp per HistorianDataSource.ReadAtTimeAsync.
|
|
var fake = new FakeHistorian(new HistorianSample
|
|
{
|
|
Value = null,
|
|
Quality = 0,
|
|
TimestampUtc = DateTime.UtcNow,
|
|
});
|
|
using var backend = BuildBackend(fake, pump);
|
|
|
|
var resp = await backend.HistoryReadAtTimeAsync(new HistoryReadAtTimeRequest
|
|
{
|
|
TagReference = "T",
|
|
TimestampsUtcUnixMs = new[] { 1L },
|
|
}, CancellationToken.None);
|
|
|
|
resp.Success.ShouldBeTrue();
|
|
resp.Values.Length.ShouldBe(1);
|
|
resp.Values[0].StatusCode.ShouldBe(0x80000000u); // Bad category
|
|
resp.Values[0].ValueBytes.ShouldBeNull();
|
|
}
|
|
|
|
private sealed class FakeHistorian : IHistorianDataSource
|
|
{
|
|
private readonly HistorianSample[] _samples;
|
|
public int Calls { get; private set; }
|
|
public DateTime[] LastTimestamps { get; private set; } = Array.Empty<DateTime>();
|
|
|
|
public FakeHistorian(params HistorianSample[] samples) => _samples = samples;
|
|
|
|
public Task<List<HistorianSample>> ReadAtTimeAsync(string tag, DateTime[] ts, CancellationToken ct)
|
|
{
|
|
Calls++;
|
|
LastTimestamps = ts;
|
|
return Task.FromResult(new List<HistorianSample>(_samples));
|
|
}
|
|
|
|
public Task<List<HistorianSample>> ReadRawAsync(string tag, DateTime s, DateTime e, int max, CancellationToken ct)
|
|
=> Task.FromResult(new List<HistorianSample>());
|
|
public Task<List<HistorianAggregateSample>> ReadAggregateAsync(string tag, DateTime s, DateTime e, double ms, string col, CancellationToken ct)
|
|
=> Task.FromResult(new List<HistorianAggregateSample>());
|
|
public Task<List<HistorianEventDto>> ReadEventsAsync(string? src, DateTime s, DateTime e, int max, CancellationToken ct)
|
|
=> Task.FromResult(new List<HistorianEventDto>());
|
|
public HistorianHealthSnapshot GetHealthSnapshot() => new();
|
|
public void Dispose() { }
|
|
}
|
|
}
|