Merge #600.1/#603: provision changed-to-historized tags; retire dormant VirtualTagEngine; cache ScriptSandbox references; one LocalDb replication predicate for registration and drain gate
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+40
@@ -146,6 +146,46 @@ public sealed class AddressSpaceApplierProvisioningTests
|
||||
prov.Seen[0].TagName.ShouldBe("Pump1.Good");
|
||||
}
|
||||
|
||||
/// <summary>#600.1: a CHANGED raw tag that flips to historized, or whose historian name / data type
|
||||
/// changes, is provisioned; an unchanged-historization edit and an alarm-bearing tag are not. The
|
||||
/// added "Control" tag is the positive control so the absences are not a vacuous no-dispatch.</summary>
|
||||
[Fact]
|
||||
public async Task Apply_provisions_changed_tags_whose_historization_flips_or_name_changes()
|
||||
{
|
||||
var prov = new CapturingProvisioner();
|
||||
var applier = new AddressSpaceApplier(NullOpcUaAddressSpaceSink.Instance, NullLogger<AddressSpaceApplier>.Instance, prov);
|
||||
|
||||
var flipOff = NonHistorizedTag("Flip", "Float32") with { TagId = "t-flip", NodeId = "Plant/Flip" };
|
||||
var flipOn = flipOff with { IsHistorized = true };
|
||||
var renOld = HistorizedTag("Ren", "Old.Name", "Float32") with { TagId = "t-ren" };
|
||||
var renNew = renOld with { HistorianTagname = "New.Name" };
|
||||
var sameOld = HistorizedTag("Same", "Same.Name", "Float32") with { TagId = "t-same", Writable = false };
|
||||
var sameNew = sameOld with { Writable = true }; // non-historian edit
|
||||
var alarmOld = NonHistorizedTag("Alm", "Float32") with { TagId = "t-alm" };
|
||||
var alarmNew = alarmOld with
|
||||
{
|
||||
IsHistorized = true,
|
||||
Alarm = new EquipmentTagAlarmInfo("AlarmCondition", 500),
|
||||
};
|
||||
|
||||
var plan = PlanWithAddedTags(HistorizedTag("Control", "Control.Name", "Float32")) with
|
||||
{
|
||||
ChangedRawTags = new[]
|
||||
{
|
||||
new AddressSpacePlan.RawTagDelta(flipOff, flipOn),
|
||||
new AddressSpacePlan.RawTagDelta(renOld, renNew),
|
||||
new AddressSpacePlan.RawTagDelta(sameOld, sameNew),
|
||||
new AddressSpacePlan.RawTagDelta(alarmOld, alarmNew),
|
||||
},
|
||||
};
|
||||
|
||||
applier.Apply(plan);
|
||||
|
||||
await prov.Called.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
|
||||
prov.Seen.Select(r => r.TagName).ShouldBe(
|
||||
new[] { "Control.Name", "Plant/Flip", "New.Name" }, ignoreOrder: true);
|
||||
}
|
||||
|
||||
/// <summary>Capturing <see cref="ILogger{T}"/> that records every log line and signals a
|
||||
/// <see cref="TaskCompletionSource{TResult}"/> the first time a "historian provisioning completed" tally is
|
||||
/// emitted, so the fire-and-forget continuation's log can be awaited deterministically.</summary>
|
||||
|
||||
+10
-5
@@ -122,14 +122,18 @@ public sealed class AlarmHistorianRegistrationTests
|
||||
/// </remarks>
|
||||
[Theory]
|
||||
// Neither key: an unpaired node. Its rows exist nowhere else.
|
||||
[InlineData(null, null, true)]
|
||||
[InlineData(null, null, null, true)]
|
||||
// The dialing half of a pair — gated, because its partner holds the same rows.
|
||||
[InlineData("http://peer:9001", null, false)]
|
||||
[InlineData("http://peer:9001", null, null, false)]
|
||||
// The LISTENING half. Same shared queue, but it never dials, so a PeerAddress-only test would
|
||||
// wrongly class it as unpaired and leave it permanently ungated.
|
||||
[InlineData(null, "9001", false)]
|
||||
// wrongly class it as unpaired and leave it permanently ungated. It is identified by the shared
|
||||
// ApiKey (the same predicate LocalDbSetup registers tables on), not by its listen port.
|
||||
[InlineData(null, "9001", "k", false)]
|
||||
// A listen port with NO key: the fail-closed passive endpoint rejects every inbound stream, so the
|
||||
// queue is shared with nobody and the node must keep draining (#603: this used to be gated).
|
||||
[InlineData(null, "9001", null, true)]
|
||||
public async Task Only_an_unreplicated_node_ignores_the_role_view(
|
||||
string? peerAddress, string? syncListenPort, bool expectDrain)
|
||||
string? peerAddress, string? syncListenPort, string? apiKey, bool expectDrain)
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), "otopcua-alarmhist-test-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempDir);
|
||||
@@ -143,6 +147,7 @@ public sealed class AlarmHistorianRegistrationTests
|
||||
["LocalDb:Path"] = dbPath,
|
||||
["LocalDb:Replication:PeerAddress"] = peerAddress,
|
||||
["LocalDb:SyncListenPort"] = syncListenPort,
|
||||
["LocalDb:Replication:ApiKey"] = apiKey,
|
||||
});
|
||||
|
||||
var services = BaseServices();
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Historian;
|
||||
|
||||
/// <summary>#603: the single "is this node's LocalDb replicated" predicate shared by table registration and the alarm drain gate.</summary>
|
||||
public sealed class LocalDbReplicationPolicyTests
|
||||
{
|
||||
private static IConfiguration Config(params (string Key, string? Value)[] pairs) =>
|
||||
new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(pairs.Select(p => new KeyValuePair<string, string?>(p.Key, p.Value)))
|
||||
.Build();
|
||||
|
||||
/// <summary>PeerAddress OR ApiKey counts; the listen port alone and blank values do not.</summary>
|
||||
[Theory]
|
||||
[InlineData("LocalDb:Replication:PeerAddress", "http://peer:9001", true)]
|
||||
[InlineData("LocalDb:Replication:ApiKey", "k", true)]
|
||||
[InlineData("LocalDb:SyncListenPort", "9001", false)]
|
||||
[InlineData("LocalDb:Replication:ApiKey", " ", false)]
|
||||
public void IsConfigured_follows_the_PeerAddress_or_ApiKey_rule(string key, string value, bool expected)
|
||||
{
|
||||
LocalDbReplicationPolicy.IsConfigured(Config((key, value))).ShouldBe(expected);
|
||||
}
|
||||
|
||||
/// <summary>No replication keys at all is an unpaired node.</summary>
|
||||
[Fact]
|
||||
public void IsConfigured_is_false_for_an_empty_configuration()
|
||||
{
|
||||
LocalDbReplicationPolicy.IsConfigured(Config()).ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user