Merge branch 'master' into b626-opcuaclient
This commit is contained in:
@@ -105,4 +105,61 @@ public sealed class S7DriverPageFormSerializationTests
|
||||
roundTripped.Probe.Timeout.ShouldBe(TimeSpan.FromSeconds(4));
|
||||
roundTripped.ProbeTimeoutSeconds.ShouldBe(20);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormModel_RoundTrip_PreservesTsapOverrides()
|
||||
{
|
||||
var opts = new S7DriverOptions
|
||||
{
|
||||
Host = "192.168.1.50",
|
||||
LocalTsap = "01.00",
|
||||
RemoteTsap = "03.01",
|
||||
};
|
||||
var back = S7DriverForm.FormModel.FromOptions(opts).ToOptions();
|
||||
back.LocalTsap.ShouldBe("01.00");
|
||||
back.RemoteTsap.ShouldBe("03.01");
|
||||
back.ConnectionType.ShouldBeNull();
|
||||
|
||||
var withType = S7DriverForm.FormModel.FromOptions(new S7DriverOptions { ConnectionType = S7ConnectionType.OP }).ToOptions();
|
||||
withType.ConnectionType.ShouldBe(S7ConnectionType.OP);
|
||||
|
||||
// Defaults stay absent: a blank form must not invent an override.
|
||||
var blank = S7DriverForm.FormModel.FromOptions(new S7DriverOptions()).ToOptions();
|
||||
blank.LocalTsap.ShouldBeNull();
|
||||
blank.RemoteTsap.ShouldBeNull();
|
||||
blank.ConnectionType.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectionType_serialises_as_a_string_the_factory_binds()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(
|
||||
new S7DriverOptions { ConnectionType = S7ConnectionType.PG },
|
||||
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new JsonStringEnumConverter() } });
|
||||
json.ShouldContain("\"connectionType\":\"PG\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_tsap_loads_into_the_form_model_instead_of_falling_back_to_defaults()
|
||||
{
|
||||
// The factory accepts a JSON number; the form binds the same document the same way, so a Save
|
||||
// cannot silently replace the authored config with defaults.
|
||||
var opts = JsonSerializer.Deserialize<S7DriverOptions>(
|
||||
"""{"remoteTsap":769,"localTsap":"01.00","cpuType":"S7300"}""",
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true, Converters = { new JsonStringEnumConverter() } });
|
||||
opts.ShouldNotBeNull();
|
||||
var back = S7DriverForm.FormModel.FromOptions(opts).ToOptions();
|
||||
back.RemoteTsap.ShouldBe("03.01");
|
||||
back.LocalTsap.ShouldBe("01.00");
|
||||
back.CpuType.ShouldBe(S7CpuType.S7300);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Form_reports_the_factory_conflict_error_for_remote_tsap_plus_connection_type()
|
||||
{
|
||||
var form = new S7DriverForm.FormModel { RemoteTsap = "03.01", ConnectionTypeText = "PG" };
|
||||
form.TsapConflictError().ShouldNotBeNull().ShouldContain(S7DriverOptions.TsapConflictDetail);
|
||||
new S7DriverForm.FormModel { RemoteTsap = "03.01" }.TsapConflictError().ShouldBeNull();
|
||||
new S7DriverForm.FormModel { ConnectionTypeText = "PG" }.TsapConflictError().ShouldBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
using ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
|
||||
|
||||
/// <summary>#244/#245/#247: one address authority (AbLegacyAddress.Validate) behind the editor and the deploy gate.</summary>
|
||||
public sealed class AbLegacyFamilyAddressGateTests
|
||||
{
|
||||
private static string Cfg(string address) => "{\"address\":\"" + address + "\",\"dataType\":\"Int\"}";
|
||||
|
||||
[Theory]
|
||||
[InlineData("{\"PlcFamily\":\"Plc5\"}", "I:018", true)] // octal 8 on PLC-5
|
||||
[InlineData("{\"PlcFamily\":\"Plc5\"}", "I:017/07", false)]
|
||||
[InlineData("{\"PlcFamily\":\"Slc500\"}", "I:018", false)] // control: decimal on SLC
|
||||
[InlineData("{\"plcFamily\":\"plc5\"}", "I:018", true)] // key + value case-insensitive
|
||||
[InlineData("{\"PlcFamily\":\"Slc500\"}", "RTC:0.HR", true)] // function file off MicroLogix
|
||||
[InlineData("{\"PlcFamily\":\"MicroLogix\"}", "RTC:0.HR", false)]
|
||||
[InlineData(null, "RTC:0.HR", true)] // absent family = runtime default SLC 500
|
||||
[InlineData("{\"PlcFamily\":\"Bogus\"}", "I:018", false)] // unknown family: never stricter than the runtime
|
||||
[InlineData("{\"PlcFamily\":\"Slc500\"}", "Q9:0", true)]
|
||||
public void Gate_checks_the_address_against_the_device_family(string? deviceConfig, string address, bool warns)
|
||||
{
|
||||
var w = EquipmentTagConfigInspector.Inspect(DriverTypeNames.AbLegacy, Cfg(address), deviceConfig);
|
||||
(w.Count > 0).ShouldBe(warns, string.Join("; ", w));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate_warns_about_indirect_addresses_which_never_read()
|
||||
{
|
||||
EquipmentTagConfigInspector.Inspect(DriverTypeNames.AbLegacy, Cfg("N7:[N7:0]"), "{\"PlcFamily\":\"Slc500\"}")
|
||||
.ShouldContain(x => x.Contains("indirect"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("N7:0", null)]
|
||||
[InlineData("I:018", null)] // family unknown to the editor: another family accepts it
|
||||
[InlineData("RTC:0.HR", null)] // MicroLogix accepts it
|
||||
[InlineData("Q9:0", "any PLC family")]
|
||||
public void Editor_validates_only_the_family_independent_shape(string address, string? error)
|
||||
{
|
||||
var result = AbLegacyTagConfigModel.FromJson(Cfg(address)).Validate();
|
||||
if (error is null) result.ShouldBeNull(); else result!.ShouldContain(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Editor_is_never_stricter_than_the_gate()
|
||||
{
|
||||
const string address = "Q9:0";
|
||||
AbLegacyTagConfigModel.FromJson(Cfg(address)).Validate().ShouldNotBeNull();
|
||||
EquipmentTagConfigInspector.Inspect(DriverTypeNames.AbLegacy, Cfg(address), null).ShouldNotBeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -328,4 +328,15 @@ public sealed class MTConnectTagConfigModelTests
|
||||
public void Warnings_is_a_noop_without_probe_metadata()
|
||||
=> MTConnectTagConfigModel.FromJson("{\"fullName\":\"x\",\"dataType\":\"Boolean\"}")
|
||||
.Warnings().ShouldBeEmpty();
|
||||
|
||||
[Fact]
|
||||
public void Warnings_accepts_a_float64_array_for_time_series_and_flags_other_array_types()
|
||||
{
|
||||
MTConnectTagConfigModel.FromJson("{\"fullName\":\"x\",\"dataType\":\"Float64\",\"isArray\":true}")
|
||||
.Warnings().ShouldBeEmpty();
|
||||
|
||||
var m = MTConnectTagConfigModel.FromJson("{\"fullName\":\"x\",\"dataType\":\"Int32\",\"isArray\":true}");
|
||||
m.Warnings().ShouldHaveSingleItem().ShouldContain("Float64");
|
||||
m.Validate().ShouldBeNull(); // advisory only
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ public sealed class RawTagConfigGateTests
|
||||
public void A_browse_committed_config_passes_the_gate(string driverType)
|
||||
{
|
||||
var tagConfig = RawBrowseCommitMapper.BuildTagConfig(
|
||||
driverType, "SomeAddress", BrowseFieldsFor(driverType));
|
||||
driverType, driverType == DriverTypeNames.AbLegacy ? "N7:0" : "SomeAddress", BrowseFieldsFor(driverType));
|
||||
|
||||
TagConfigValidator.Validate(driverType, tagConfig).ShouldBeNull(
|
||||
$"browse-commit for {driverType} must not author a config the import refuses");
|
||||
|
||||
@@ -421,6 +421,25 @@ public sealed class RawTreeServiceWaveBTests
|
||||
result.Error!.ShouldContain("already exists");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateDevice_refuses_a_second_device_under_an_MTConnect_driver_but_not_under_others()
|
||||
{
|
||||
var (service, dbName) = Seeded();
|
||||
var mt = await service.CreateDriverAsync(RawTreeTestDb.ClusterId, null, "mt-1", "MTConnect", "{}");
|
||||
mt.Ok.ShouldBeTrue();
|
||||
|
||||
// CreateDriver auto-creates Device1, so the very next device is the ambiguous second one (#628).
|
||||
var refused = await service.CreateDeviceAsync(mt.CreatedId!, "Device2", "{}");
|
||||
|
||||
refused.Ok.ShouldBeFalse();
|
||||
refused.Error!.ShouldContain("one MTConnect driver per agent device");
|
||||
|
||||
// Positive control: the same call against a non-MTConnect driver still succeeds.
|
||||
(await service.CreateDeviceAsync(RawTreeTestDb.FolderedDriverId, "Device9", "{}")).Ok.ShouldBeTrue();
|
||||
using var db = RawTreeTestDb.CreateNamed(dbName);
|
||||
db.Devices.Count(d => d.DriverInstanceId == mt.CreatedId).ShouldBe(1);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- Merged probe config (Test connect)
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -112,6 +112,14 @@ public sealed class S7TagConfigModelTests
|
||||
[InlineData("Float64")]
|
||||
[InlineData("String")]
|
||||
[InlineData("DateTime")]
|
||||
[InlineData("WString")]
|
||||
[InlineData("Char")]
|
||||
[InlineData("WChar")]
|
||||
[InlineData("Dtl")]
|
||||
[InlineData("Date")]
|
||||
[InlineData("Tod")]
|
||||
[InlineData("Time")]
|
||||
[InlineData("S5Time")]
|
||||
public void Validate_rejects_wide_type_arrays(string dataType)
|
||||
{
|
||||
var m = S7TagConfigModel.FromJson(
|
||||
|
||||
Reference in New Issue
Block a user