refactor(historian-gateway): retire Wonderware historian projects (gateway is sole backend)
The HistorianGateway driver is now the sole historian read/write+alarm backend, so the Wonderware sidecar projects are dead code. Removes the 5 Wonderware projects (driver, .Client, .Client.Contracts, + their 2 test projects) from the solution and tree, and fully retires the vestigial 'Historian.Wonderware' driver type (UI/probe-only; it had no driver factory): the Host probe registration, the AdminUI driver-config surface (driver page, tag-config editor/model/validator entry, address picker/builder, driver-type catalog + dropdown + edit-router entries), and their tests. Prunes the now-unused Wonderware connection fields (Host/Port/UseTls/ServerCertThumbprint/SharedSecret) from AlarmHistorianOptions (keeping Enabled + the SQLite store-and-forward knobs) and refreshes the stale XML docs that named Wonderware as the production backend. Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
@@ -62,7 +62,7 @@ public sealed class DriverPageJsonConverterTests
|
||||
var allDriverPages = typeof(S7DriverPage).Assembly.GetTypes()
|
||||
.Where(t => t.Name.EndsWith("DriverPage", StringComparison.Ordinal) && !t.IsAbstract)
|
||||
.ToList();
|
||||
allDriverPages.Count.ShouldBeGreaterThanOrEqualTo(9, "reflection should discover the full driver-page fleet");
|
||||
allDriverPages.Count.ShouldBeGreaterThanOrEqualTo(8, "reflection should discover the full driver-page fleet");
|
||||
DriverPageTypes.Count.ShouldBe(allDriverPages.Count,
|
||||
"every *DriverPage must declare a _jsonOpts config serializer so the string-enum converter guard covers it");
|
||||
}
|
||||
|
||||
-129
@@ -1,129 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Clusters.Drivers;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests;
|
||||
|
||||
public sealed class HistorianWonderwareDriverPageFormSerializationTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions _opts = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_PreservesKnownFields()
|
||||
{
|
||||
var original = new WonderwareHistorianClientOptions(
|
||||
Host: "historian-prod.zb.local",
|
||||
Port: 32569,
|
||||
SharedSecret: "t0ps3cr3t",
|
||||
PeerName: "OtOpcUa-Primary",
|
||||
ConnectTimeout: TimeSpan.FromSeconds(20),
|
||||
CallTimeout: TimeSpan.FromSeconds(60))
|
||||
{
|
||||
ProbeTimeoutSeconds = 25,
|
||||
UseTls = true,
|
||||
ServerCertThumbprint = "A1B2C3D4E5F60718293A4B5C6D7E8F9012345678",
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(original, _opts);
|
||||
var back = JsonSerializer.Deserialize<WonderwareHistorianClientOptions>(json, _opts);
|
||||
|
||||
back.ShouldNotBeNull();
|
||||
back.Host.ShouldBe("historian-prod.zb.local");
|
||||
back.Port.ShouldBe(32569);
|
||||
back.SharedSecret.ShouldBe("t0ps3cr3t");
|
||||
back.PeerName.ShouldBe("OtOpcUa-Primary");
|
||||
back.ConnectTimeout.ShouldBe(TimeSpan.FromSeconds(20));
|
||||
back.CallTimeout.ShouldBe(TimeSpan.FromSeconds(60));
|
||||
back.EffectiveConnectTimeout.ShouldBe(TimeSpan.FromSeconds(20));
|
||||
back.EffectiveCallTimeout.ShouldBe(TimeSpan.FromSeconds(60));
|
||||
back.ProbeTimeoutSeconds.ShouldBe(25);
|
||||
back.UseTls.ShouldBeTrue();
|
||||
back.ServerCertThumbprint.ShouldBe("A1B2C3D4E5F60718293A4B5C6D7E8F9012345678");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_NullTimeouts_UsesDefaults()
|
||||
{
|
||||
var original = new WonderwareHistorianClientOptions(
|
||||
Host: "localhost",
|
||||
Port: 32569,
|
||||
SharedSecret: "secret");
|
||||
|
||||
var json = JsonSerializer.Serialize(original, _opts);
|
||||
var back = JsonSerializer.Deserialize<WonderwareHistorianClientOptions>(json, _opts);
|
||||
|
||||
back.ShouldNotBeNull();
|
||||
back.ConnectTimeout.ShouldBeNull();
|
||||
back.CallTimeout.ShouldBeNull();
|
||||
back.EffectiveConnectTimeout.ShouldBe(TimeSpan.FromSeconds(10));
|
||||
back.EffectiveCallTimeout.ShouldBe(TimeSpan.FromSeconds(30));
|
||||
back.UseTls.ShouldBeFalse();
|
||||
back.ServerCertThumbprint.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_DropsUnknownFields()
|
||||
{
|
||||
var jsonWithExtra = """
|
||||
{
|
||||
"unknownField": "old-value",
|
||||
"host": "historian.zb.local",
|
||||
"port": 32569,
|
||||
"sharedSecret": "s3cr3t",
|
||||
"probeTimeoutSeconds": 20
|
||||
}
|
||||
""";
|
||||
|
||||
var optsWithSkip = new JsonSerializerOptions(_opts)
|
||||
{
|
||||
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
|
||||
};
|
||||
|
||||
var back = JsonSerializer.Deserialize<WonderwareHistorianClientOptions>(jsonWithExtra, optsWithSkip);
|
||||
back.ShouldNotBeNull();
|
||||
back.ProbeTimeoutSeconds.ShouldBe(20);
|
||||
back.Host.ShouldBe("historian.zb.local");
|
||||
back.Port.ShouldBe(32569);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormModel_RoundTrip_PreservesAllFields()
|
||||
{
|
||||
// Construct a record with non-default values for every property and verify
|
||||
// that WonderwareHistorianClientFormModel.FromRecord → ToRecord is lossless.
|
||||
var original = new WonderwareHistorianClientOptions(
|
||||
Host: "historian-prod.zb.local",
|
||||
Port: 32570,
|
||||
SharedSecret: "sup3rs3cr3t",
|
||||
PeerName: "OtOpcUa-Redundant",
|
||||
ConnectTimeout: TimeSpan.FromSeconds(18),
|
||||
CallTimeout: TimeSpan.FromSeconds(45))
|
||||
{
|
||||
ProbeTimeoutSeconds = 30,
|
||||
UseTls = true,
|
||||
ServerCertThumbprint = "0011223344556677889AABBCCDDEEFF001122334",
|
||||
};
|
||||
|
||||
var form = HistorianWonderwareDriverPage.WonderwareHistorianClientFormModel.FromRecord(original);
|
||||
var result = form.ToRecord();
|
||||
|
||||
result.Host.ShouldBe("historian-prod.zb.local");
|
||||
result.Port.ShouldBe(32570);
|
||||
result.SharedSecret.ShouldBe("sup3rs3cr3t");
|
||||
result.PeerName.ShouldBe("OtOpcUa-Redundant");
|
||||
result.ConnectTimeout.ShouldBe(TimeSpan.FromSeconds(18));
|
||||
result.CallTimeout.ShouldBe(TimeSpan.FromSeconds(45));
|
||||
result.EffectiveConnectTimeout.ShouldBe(TimeSpan.FromSeconds(18));
|
||||
result.EffectiveCallTimeout.ShouldBe(TimeSpan.FromSeconds(45));
|
||||
result.ProbeTimeoutSeconds.ShouldBe(30);
|
||||
result.UseTls.ShouldBeTrue();
|
||||
result.ServerCertThumbprint.ShouldBe("0011223344556677889AABBCCDDEEFF001122334");
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Pickers;
|
||||
|
||||
public sealed class HistorianWonderwareAddressBuilderTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("SysTimeHour", "Cyclic", 60, "SysTimeHour?mode=Cyclic&interval=60")]
|
||||
[InlineData("ReactorTemp", "Last", 1, "ReactorTemp?mode=Last&interval=1")]
|
||||
[InlineData("FlowRate", "Delta", 30, "FlowRate?mode=Delta&interval=30")]
|
||||
public void Build_Canonical(string tag, string mode, int interval, string expected)
|
||||
=> HistorianWonderwareAddressBuilder.Build(tag, mode, interval).ShouldBe(expected);
|
||||
|
||||
/// <summary>A tag name carrying query-reserved characters is percent-encoded so the produced
|
||||
/// address stays a well-formed query string (AdminUI-005). With "A&B?C" the '&' and '?'
|
||||
/// must not be read as a query separator / start, so they are escaped.</summary>
|
||||
[Fact]
|
||||
public void Build_escapes_reserved_characters_in_tag_name()
|
||||
{
|
||||
var result = HistorianWonderwareAddressBuilder.Build("A&B?C", "Cyclic", 60);
|
||||
|
||||
// The only literal '?' is the query separator the builder inserts; the only literal '&'
|
||||
// is the one between mode and interval. The reserved characters in the name are escaped.
|
||||
result.ShouldBe("A%26B%3FC?mode=Cyclic&interval=60");
|
||||
result.IndexOf('?').ShouldBe(result.IndexOf("?mode=", System.StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
public sealed class HistorianWonderwareTagConfigModelTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("{}")]
|
||||
public void FromJson_returns_defaults_for_empty_input(string? json)
|
||||
{
|
||||
var m = HistorianWonderwareTagConfigModel.FromJson(json);
|
||||
|
||||
m.FullName.ShouldBe("");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_reads_FullName()
|
||||
{
|
||||
var m = HistorianWonderwareTagConfigModel.FromJson(
|
||||
"""{"FullName":"Reactor1.Temp"}""");
|
||||
|
||||
m.FullName.ShouldBe("Reactor1.Temp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Round_trip_preserves_FullName()
|
||||
{
|
||||
var m = new HistorianWonderwareTagConfigModel { FullName = "Reactor1.Temp" };
|
||||
|
||||
var json = m.ToJson();
|
||||
var m2 = HistorianWonderwareTagConfigModel.FromJson(json);
|
||||
|
||||
m2.FullName.ShouldBe("Reactor1.Temp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToJson_emits_PascalCase_FullName()
|
||||
{
|
||||
var m = new HistorianWonderwareTagConfigModel { FullName = "Reactor1.Temp" };
|
||||
|
||||
var json = m.ToJson();
|
||||
|
||||
// FullName is the composer/walker contract key — PascalCase, case-sensitive.
|
||||
json.ShouldContain("\"FullName\":\"Reactor1.Temp\"");
|
||||
json.ShouldNotContain("\"fullName\"", Case.Sensitive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_then_ToJson_preserves_unknown_keys()
|
||||
{
|
||||
var json = HistorianWonderwareTagConfigModel
|
||||
.FromJson("""{"FullName":"Reactor1.Temp","deadband":0.5}""")
|
||||
.ToJson();
|
||||
|
||||
json.ShouldContain("deadband");
|
||||
json.ShouldContain("0.5");
|
||||
// and the exposed field still round-trips
|
||||
json.ShouldContain("\"FullName\":\"Reactor1.Temp\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromJson_then_ToJson_preserves_TagModal_merged_history_keys()
|
||||
{
|
||||
// The TagModal-merge seam writes isHistorized/historianTagname at the TagConfig root; this model
|
||||
// does NOT model them, so they must survive a load→save untouched as preserved unknown keys.
|
||||
var json = HistorianWonderwareTagConfigModel
|
||||
.FromJson("""{"FullName":"Reactor1.Temp","isHistorized":true,"historianTagname":"Reactor1.Temp.Override"}""")
|
||||
.ToJson();
|
||||
|
||||
json.ShouldContain("\"isHistorized\":true");
|
||||
json.ShouldContain("\"historianTagname\":\"Reactor1.Temp.Override\"");
|
||||
json.ShouldContain("\"FullName\":\"Reactor1.Temp\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToJson_trims_FullName()
|
||||
{
|
||||
var json = new HistorianWonderwareTagConfigModel { FullName = " Reactor1.Temp " }.ToJson();
|
||||
|
||||
json.ShouldContain("\"FullName\":\"Reactor1.Temp\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_returns_error_when_FullName_blank()
|
||||
{
|
||||
new HistorianWonderwareTagConfigModel().Validate().ShouldNotBeNullOrEmpty();
|
||||
new HistorianWonderwareTagConfigModel { FullName = " " }.Validate().ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_returns_null_when_FullName_present()
|
||||
{
|
||||
new HistorianWonderwareTagConfigModel { FullName = "Reactor1.Temp" }.Validate().ShouldBeNull();
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ public sealed class TagConfigValidatorTests
|
||||
[InlineData("TwinCat")]
|
||||
[InlineData("Focas")]
|
||||
[InlineData("OpcUaClient")]
|
||||
[InlineData("Historian.Wonderware")]
|
||||
public void Required_field_blank_is_rejected(string driverType)
|
||||
{
|
||||
TagConfigValidator.Validate(driverType, "{}").ShouldNotBeNullOrEmpty();
|
||||
@@ -42,10 +41,6 @@ public sealed class TagConfigValidatorTests
|
||||
public void OpcUaClient_with_full_name_is_valid()
|
||||
=> TagConfigValidator.Validate("OpcUaClient", """{"FullName":"ns=2;s=Line3.Temp"}""").ShouldBeNull();
|
||||
|
||||
[Fact]
|
||||
public void HistorianWonderware_with_full_name_is_valid()
|
||||
=> TagConfigValidator.Validate("Historian.Wonderware", """{"FullName":"Reactor1.Temp"}""").ShouldBeNull();
|
||||
|
||||
[Fact]
|
||||
public void S7_with_address_is_valid()
|
||||
=> TagConfigValidator.Validate("S7", """{"address":"DB1.DBW0"}""").ShouldBeNull();
|
||||
|
||||
@@ -29,7 +29,6 @@ public sealed class DriverProbeRegistrationTests
|
||||
"Focas", // page key; probe reports "FOCAS" — must resolve case-insensitively
|
||||
"OpcUaClient",
|
||||
"GalaxyMxGateway",
|
||||
"Historian.Wonderware",
|
||||
];
|
||||
|
||||
[Fact]
|
||||
|
||||
+9
-36
@@ -112,81 +112,54 @@ public sealed class AlarmHistorianRegistrationTests
|
||||
opts.DeadLetterRetentionDays.ShouldBe(7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_warns_on_empty_shared_secret_when_enabled()
|
||||
{
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "", DatabasePath = "/var/h.db" };
|
||||
opts.Validate().ShouldContain(w => w.Contains("SharedSecret"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_warns_on_relative_database_path_when_enabled()
|
||||
{
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "alarm-historian.db" };
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, DatabasePath = "alarm-historian.db" };
|
||||
opts.Validate().ShouldContain(w => w.Contains("DatabasePath"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_is_silent_when_correctly_configured()
|
||||
{
|
||||
new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db" }.Validate().ShouldBeEmpty();
|
||||
new AlarmHistorianOptions { Enabled = true, DatabasePath = "/abs/h.db" }.Validate().ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_is_silent_when_disabled()
|
||||
{
|
||||
new AlarmHistorianOptions { Enabled = false, SharedSecret = "" }.Validate().ShouldBeEmpty();
|
||||
new AlarmHistorianOptions { Enabled = false }.Validate().ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_warns_on_non_positive_drain_interval()
|
||||
{
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db", DrainIntervalSeconds = 0 };
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, DatabasePath = "/abs/h.db", DrainIntervalSeconds = 0 };
|
||||
opts.Validate().ShouldContain(w => w.Contains("DrainIntervalSeconds"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_warns_on_non_positive_capacity()
|
||||
{
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db", Capacity = 0 };
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, DatabasePath = "/abs/h.db", Capacity = 0 };
|
||||
opts.Validate().ShouldContain(w => w.Contains("Capacity"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_warns_on_non_positive_retention()
|
||||
{
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "s", DatabasePath = "/abs/h.db", DeadLetterRetentionDays = 0 };
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, DatabasePath = "/abs/h.db", DeadLetterRetentionDays = 0 };
|
||||
opts.Validate().ShouldContain(w => w.Contains("DeadLetterRetentionDays"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_accumulates_multiple_warnings()
|
||||
{
|
||||
// relative path + empty secret ⇒ both warnings, not short-circuited on the first.
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, SharedSecret = "", DatabasePath = "alarm-historian.db" };
|
||||
// relative path + non-positive drain interval ⇒ both warnings, not short-circuited on the first.
|
||||
var opts = new AlarmHistorianOptions { Enabled = true, DatabasePath = "alarm-historian.db", DrainIntervalSeconds = 0 };
|
||||
var warnings = opts.Validate();
|
||||
warnings.ShouldContain(w => w.Contains("SharedSecret"));
|
||||
warnings.ShouldContain(w => w.Contains("DatabasePath"));
|
||||
warnings.ShouldContain(w => w.Contains("DrainIntervalSeconds"));
|
||||
warnings.Count.ShouldBeGreaterThanOrEqualTo(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Section_binds_tcp_host_port_tls_fields()
|
||||
{
|
||||
var config = ConfigFrom(new Dictionary<string, string?>
|
||||
{
|
||||
["AlarmHistorian:Host"] = "historian.example.com",
|
||||
["AlarmHistorian:Port"] = "12345",
|
||||
["AlarmHistorian:UseTls"] = "true",
|
||||
["AlarmHistorian:ServerCertThumbprint"] = "AABBCCDDEEFF",
|
||||
});
|
||||
|
||||
var opts = config.GetSection(AlarmHistorianOptions.SectionName).Get<AlarmHistorianOptions>();
|
||||
|
||||
opts.ShouldNotBeNull();
|
||||
opts.Host.ShouldBe("historian.example.com");
|
||||
opts.Port.ShouldBe(12345);
|
||||
opts.UseTls.ShouldBeTrue();
|
||||
opts.ServerCertThumbprint.ShouldBe("AABBCCDDEEFF");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user