Phase 2 PR 11 — HistoryReadEvents IPC (alarm history). New Shared.Contracts messages HistoryReadEventsRequest/Response + GalaxyHistoricalEvent DTO (MessageKind 0x66/0x67). IGalaxyBackend gains HistoryReadEventsAsync, Stub/DbBacked return canonical pending error, MxAccessGalaxyBackend delegates to _historian.ReadEventsAsync (ported in PR 5) and maps HistorianEventDto → GalaxyHistoricalEvent — Guid.ToString() for EventId wire shape, DateTime → Unix ms for both EventTime (when the event fired in the process) and ReceivedTime (when the Historian persisted it), DisplayText + Severity pass through. SourceName is string? — null means 'all sources' (passed straight through to HistorianDataSource.ReadEventsAsync which adds the AddEventFilter('Source', Equal, ...) only when non-null). Distinct from the live GalaxyAlarmEvent type because historical rows carry both timestamps and lack StateTransition (Historian logs instantaneous events, not the OPC UA Part 9 alarm lifecycle; translating to OPC UA event lifecycle is the alarm-subsystem's job). Guards: null historian → Historian-disabled error; SDK exception → Success=false with message chained. Tests (3 new): disabled-error when historian null, maps HistorianEventDto with full field set (Id/Source/EventTime/ReceivedTime/DisplayText/Severity=900) to GalaxyHistoricalEvent, null SourceName passes through unchanged (verifies the 'all sources' contract). Galaxy.Host.Tests Unit suite 34 pass / 0 fail. Galaxy.Host builds clean. Branches off phase-2-pr10-history-attime since both extend the MessageKind enum; fast-forwards if PR 10 merges first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
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 HistoryReadEventsTests
|
||||
{
|
||||
private static MxAccessGalaxyBackend BuildBackend(IHistorianDataSource? h, StaPump pump) =>
|
||||
new(
|
||||
new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }),
|
||||
new MxAccessClient(pump, new MxProxyAdapter(), "events-test"),
|
||||
h);
|
||||
|
||||
[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.HistoryReadEventsAsync(new HistoryReadEventsRequest
|
||||
{
|
||||
SourceName = "TankA",
|
||||
StartUtcUnixMs = 0,
|
||||
EndUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
MaxEvents = 100,
|
||||
}, CancellationToken.None);
|
||||
|
||||
resp.Success.ShouldBeFalse();
|
||||
resp.Error.ShouldContain("Historian disabled");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Maps_HistorianEventDto_to_GalaxyHistoricalEvent_wire_shape()
|
||||
{
|
||||
using var pump = new StaPump("Test.Sta");
|
||||
await pump.WaitForStartedAsync();
|
||||
|
||||
var eventId = Guid.NewGuid();
|
||||
var eventTime = new DateTime(2026, 4, 18, 10, 0, 0, DateTimeKind.Utc);
|
||||
var receivedTime = eventTime.AddMilliseconds(150);
|
||||
var fake = new FakeHistorian(new HistorianEventDto
|
||||
{
|
||||
Id = eventId,
|
||||
Source = "TankA.Level.HiHi",
|
||||
EventTime = eventTime,
|
||||
ReceivedTime = receivedTime,
|
||||
DisplayText = "HiHi alarm tripped",
|
||||
Severity = 900,
|
||||
});
|
||||
using var backend = BuildBackend(fake, pump);
|
||||
|
||||
var resp = await backend.HistoryReadEventsAsync(new HistoryReadEventsRequest
|
||||
{
|
||||
SourceName = "TankA.Level.HiHi",
|
||||
StartUtcUnixMs = 0,
|
||||
EndUtcUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
MaxEvents = 50,
|
||||
}, CancellationToken.None);
|
||||
|
||||
resp.Success.ShouldBeTrue();
|
||||
resp.Events.Length.ShouldBe(1);
|
||||
var got = resp.Events[0];
|
||||
got.EventId.ShouldBe(eventId.ToString());
|
||||
got.SourceName.ShouldBe("TankA.Level.HiHi");
|
||||
got.DisplayText.ShouldBe("HiHi alarm tripped");
|
||||
got.Severity.ShouldBe<ushort>(900);
|
||||
got.EventTimeUtcUnixMs.ShouldBe(new DateTimeOffset(eventTime, TimeSpan.Zero).ToUnixTimeMilliseconds());
|
||||
got.ReceivedTimeUtcUnixMs.ShouldBe(new DateTimeOffset(receivedTime, TimeSpan.Zero).ToUnixTimeMilliseconds());
|
||||
|
||||
fake.LastSourceName.ShouldBe("TankA.Level.HiHi");
|
||||
fake.LastMaxEvents.ShouldBe(50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Null_source_name_is_passed_through_as_all_sources()
|
||||
{
|
||||
using var pump = new StaPump("Test.Sta");
|
||||
await pump.WaitForStartedAsync();
|
||||
var fake = new FakeHistorian();
|
||||
using var backend = BuildBackend(fake, pump);
|
||||
|
||||
await backend.HistoryReadEventsAsync(new HistoryReadEventsRequest
|
||||
{
|
||||
SourceName = null,
|
||||
StartUtcUnixMs = 0,
|
||||
EndUtcUnixMs = 1,
|
||||
MaxEvents = 10,
|
||||
}, CancellationToken.None);
|
||||
|
||||
fake.LastSourceName.ShouldBeNull();
|
||||
}
|
||||
|
||||
private sealed class FakeHistorian : IHistorianDataSource
|
||||
{
|
||||
private readonly HistorianEventDto[] _events;
|
||||
public string? LastSourceName { get; private set; } = "<unset>";
|
||||
public int LastMaxEvents { get; private set; }
|
||||
|
||||
public FakeHistorian(params HistorianEventDto[] events) => _events = events;
|
||||
|
||||
public Task<List<HistorianEventDto>> ReadEventsAsync(string? src, DateTime s, DateTime e, int max, CancellationToken ct)
|
||||
{
|
||||
LastSourceName = src;
|
||||
LastMaxEvents = max;
|
||||
return Task.FromResult(new List<HistorianEventDto>(_events));
|
||||
}
|
||||
|
||||
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<HistorianSample>> ReadAtTimeAsync(string tag, DateTime[] ts, CancellationToken ct)
|
||||
=> Task.FromResult(new List<HistorianSample>());
|
||||
public HistorianHealthSnapshot GetHealthSnapshot() => new();
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user